feat: 完善对象存储上传与真实条码二维码引擎,彻底剥离保底Mock数据
这个提交包含在:
@@ -1,3 +1,5 @@
|
||||
import { UserRoleFlag } from '../auth/auth-role.manager';
|
||||
|
||||
export enum ActivityPayType {
|
||||
AA_PAY = 1, // AA制付费
|
||||
PROJECT_PAY = 2, // 项目制付费
|
||||
@@ -12,8 +14,18 @@ export interface Activity {
|
||||
payType: ActivityPayType;
|
||||
totalFeeCents?: number;
|
||||
aaFeeCents?: number;
|
||||
perPersonFeeCents?: number;
|
||||
allowDynamicPrice?: boolean;
|
||||
priceAdjustRule?: 'UNIFIED_HIGH' | 'TIERED';
|
||||
priceAdjustHistory?: Array<{
|
||||
oldPriceCents: number;
|
||||
newPriceCents: number;
|
||||
adjustedAt: Date;
|
||||
operatorId: number;
|
||||
}>;
|
||||
maxParticipants: number;
|
||||
currentParticipants: number;
|
||||
status?: 'RECRUITING' | 'FULL' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED';
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
@@ -39,8 +51,15 @@ export class ActivityService {
|
||||
payType: ActivityPayType;
|
||||
totalFeeCents?: number;
|
||||
aaFeeCents?: number;
|
||||
perPersonFeeCents?: number;
|
||||
allowDynamicPrice?: boolean;
|
||||
priceAdjustRule?: 'UNIFIED_HIGH' | 'TIERED';
|
||||
maxParticipants: number;
|
||||
roleManager?: any;
|
||||
}): Activity {
|
||||
if (params.roleManager && typeof params.roleManager.grantRole === 'function') {
|
||||
params.roleManager.grantRole(params.creatorId, UserRoleFlag.HOST);
|
||||
}
|
||||
const id = `act_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
|
||||
const activity: Activity = {
|
||||
id,
|
||||
@@ -51,8 +70,13 @@ export class ActivityService {
|
||||
payType: params.payType,
|
||||
totalFeeCents: params.totalFeeCents,
|
||||
aaFeeCents: params.aaFeeCents,
|
||||
perPersonFeeCents: params.perPersonFeeCents,
|
||||
allowDynamicPrice: params.allowDynamicPrice ?? true,
|
||||
priceAdjustRule: params.priceAdjustRule || 'UNIFIED_HIGH',
|
||||
priceAdjustHistory: [],
|
||||
maxParticipants: params.maxParticipants,
|
||||
currentParticipants: 0,
|
||||
status: 'RECRUITING',
|
||||
};
|
||||
this.activities.set(id, activity);
|
||||
this.participants.set(id, []);
|
||||
@@ -133,4 +157,186 @@ export class ActivityService {
|
||||
|
||||
return { success: true, participant };
|
||||
}
|
||||
|
||||
adjustActivityPrice(params: {
|
||||
activityId: string;
|
||||
operatorId: number;
|
||||
newPriceCents: number;
|
||||
adjustRule?: 'UNIFIED_HIGH' | 'TIERED';
|
||||
}): { activity: Activity; priceDiffCents: number; affectedParticipantsCount: number } {
|
||||
const activity = this.activities.get(params.activityId);
|
||||
if (!activity) {
|
||||
throw new Error('活动不存在');
|
||||
}
|
||||
if (activity.creatorId !== params.operatorId) {
|
||||
throw new Error('只有发起人有权调整活动出价');
|
||||
}
|
||||
if (activity.allowDynamicPrice === false) {
|
||||
throw new Error('该活动未开启动态调价权限');
|
||||
}
|
||||
if (params.newPriceCents <= 0) {
|
||||
throw new Error('调整后的价格必须大于0');
|
||||
}
|
||||
|
||||
const currentPriceCents = activity.perPersonFeeCents || activity.aaFeeCents || 0;
|
||||
const priceDiffCents = params.newPriceCents - currentPriceCents;
|
||||
const rule = params.adjustRule || activity.priceAdjustRule || 'UNIFIED_HIGH';
|
||||
|
||||
const currentList = this.participants.get(params.activityId) || [];
|
||||
let affectedParticipantsCount = 0;
|
||||
|
||||
if (rule === 'UNIFIED_HIGH') {
|
||||
for (const p of currentList) {
|
||||
p.payAmountCents = params.newPriceCents;
|
||||
}
|
||||
affectedParticipantsCount = currentList.length;
|
||||
}
|
||||
|
||||
if (!activity.priceAdjustHistory) {
|
||||
activity.priceAdjustHistory = [];
|
||||
}
|
||||
activity.priceAdjustHistory.push({
|
||||
oldPriceCents: currentPriceCents,
|
||||
newPriceCents: params.newPriceCents,
|
||||
adjustedAt: new Date(),
|
||||
operatorId: params.operatorId,
|
||||
});
|
||||
|
||||
if (activity.payType === ActivityPayType.AA_PAY) {
|
||||
activity.aaFeeCents = params.newPriceCents;
|
||||
activity.totalFeeCents = params.newPriceCents * Math.max(1, activity.maxParticipants);
|
||||
} else {
|
||||
activity.perPersonFeeCents = params.newPriceCents;
|
||||
activity.aaFeeCents = params.newPriceCents;
|
||||
activity.totalFeeCents = params.newPriceCents * Math.max(1, activity.maxParticipants);
|
||||
}
|
||||
|
||||
return { activity, priceDiffCents, affectedParticipantsCount };
|
||||
}
|
||||
|
||||
getAllActivities(): Activity[] {
|
||||
return Array.from(this.activities.values());
|
||||
}
|
||||
|
||||
getActivity(id: string): Activity | undefined {
|
||||
return this.activities.get(id);
|
||||
}
|
||||
|
||||
getParticipants(activityId: string): Participant[] {
|
||||
return this.participants.get(activityId) || [];
|
||||
}
|
||||
|
||||
cancelActivity(params: {
|
||||
activityId: string;
|
||||
operatorId: number;
|
||||
reason?: string;
|
||||
}): {
|
||||
success: boolean;
|
||||
status: string;
|
||||
refundedCount: number;
|
||||
totalRefundCents: number;
|
||||
refundDetails: Array<{ userId: number; refundAmountCents: number }>;
|
||||
} {
|
||||
const activity = this.activities.get(params.activityId);
|
||||
if (!activity) {
|
||||
throw new Error('活动不存在');
|
||||
}
|
||||
|
||||
if (activity.creatorId !== params.operatorId) {
|
||||
throw new Error('只有发起人有权解散此组局');
|
||||
}
|
||||
|
||||
const currentList = this.participants.get(params.activityId) || [];
|
||||
const hasCheckedIn = currentList.some((p) => p.checkinStatus);
|
||||
if (hasCheckedIn) {
|
||||
throw new Error('聚会已有成员到场核销,不可随意解散');
|
||||
}
|
||||
|
||||
const refundDetails = currentList.map((p) => ({
|
||||
userId: p.userId,
|
||||
refundAmountCents: p.payAmountCents,
|
||||
}));
|
||||
const totalRefundCents = refundDetails.reduce((sum, r) => sum + r.refundAmountCents, 0);
|
||||
|
||||
activity.status = 'CANCELLED';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: 'CANCELLED',
|
||||
refundedCount: refundDetails.length,
|
||||
totalRefundCents,
|
||||
refundDetails,
|
||||
};
|
||||
}
|
||||
|
||||
leaveActivity(params: {
|
||||
activityId: string;
|
||||
userId: number;
|
||||
}): {
|
||||
success: boolean;
|
||||
refundAmountCents: number;
|
||||
} {
|
||||
const activity = this.activities.get(params.activityId);
|
||||
if (!activity) {
|
||||
throw new Error('活动不存在');
|
||||
}
|
||||
|
||||
const currentList = this.participants.get(params.activityId) || [];
|
||||
const ptIndex = currentList.findIndex((p) => p.userId === params.userId);
|
||||
if (ptIndex === -1) {
|
||||
throw new Error('用户未报名该活动');
|
||||
}
|
||||
|
||||
const pt = currentList[ptIndex];
|
||||
if (pt.checkinStatus) {
|
||||
throw new Error('已核销到场,无法申请退席');
|
||||
}
|
||||
|
||||
const refundAmountCents = pt.payAmountCents;
|
||||
currentList.splice(ptIndex, 1);
|
||||
activity.currentParticipants = Math.max(0, activity.currentParticipants - 1);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
refundAmountCents,
|
||||
};
|
||||
}
|
||||
|
||||
adminForceCancelActivity(params: {
|
||||
activityId: string;
|
||||
operatorAdminId: number;
|
||||
reason?: string;
|
||||
roleManager: any;
|
||||
}): {
|
||||
success: boolean;
|
||||
status: string;
|
||||
refundedCount: number;
|
||||
totalRefundCents: number;
|
||||
refundDetails: Array<{ userId: number; refundAmountCents: number }>;
|
||||
} {
|
||||
if (!params.roleManager || !params.roleManager.isAdmin(params.operatorAdminId)) {
|
||||
throw new Error('只有平台管理员有权执行平台强制解散');
|
||||
}
|
||||
const activity = this.activities.get(params.activityId);
|
||||
if (!activity) {
|
||||
throw new Error('活动不存在');
|
||||
}
|
||||
const currentList = this.participants.get(params.activityId) || [];
|
||||
const refundDetails = currentList.map((p) => ({
|
||||
userId: p.userId,
|
||||
refundAmountCents: p.payAmountCents,
|
||||
}));
|
||||
const totalRefundCents = refundDetails.reduce((sum, r) => sum + r.refundAmountCents, 0);
|
||||
|
||||
activity.status = 'CANCELLED';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: 'CANCELLED',
|
||||
refundedCount: refundDetails.length,
|
||||
totalRefundCents,
|
||||
refundDetails,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export enum UserRoleFlag {
|
||||
NONE = 0,
|
||||
PLAYER = 1 << 0, // 1
|
||||
EXPERT = 1 << 1, // 2
|
||||
PLAYER = 1 << 0, // 1: 普通玩家/买家
|
||||
EXPERT = 1 << 1, // 2: 陪玩大神/接单人
|
||||
HOST = 1 << 2, // 4: 车队房主/局长
|
||||
ADMIN = 1 << 3, // 8: 平台管理员/运营端
|
||||
}
|
||||
|
||||
export enum WorkMode {
|
||||
@@ -24,57 +26,181 @@ export interface UserAccount {
|
||||
currentMode: WorkMode;
|
||||
}
|
||||
|
||||
export interface ExpertCertRecord {
|
||||
userId: number;
|
||||
status: CertStatus;
|
||||
realName?: string;
|
||||
idCardNo?: string;
|
||||
rejectReason?: string;
|
||||
appliedAt: Date;
|
||||
auditedAt?: Date;
|
||||
auditedBy?: number;
|
||||
}
|
||||
|
||||
export class AuthRoleManager {
|
||||
private users = new Map<number, UserAccount>();
|
||||
private certs = new Map<number, { status: CertStatus; realName?: string; idCardNo?: string }>();
|
||||
private certs = new Map<number, ExpertCertRecord>();
|
||||
private autoIncId = 1000;
|
||||
|
||||
registerUser(params: { openid: string; nickname: string }): UserAccount {
|
||||
registerUser(params: { openid: string; nickname: string; initialRoles?: number }): UserAccount {
|
||||
const id = ++this.autoIncId;
|
||||
const user: UserAccount = {
|
||||
id,
|
||||
openid: params.openid,
|
||||
nickname: params.nickname,
|
||||
roleFlags: UserRoleFlag.PLAYER,
|
||||
roleFlags: params.initialRoles ?? UserRoleFlag.PLAYER,
|
||||
currentMode: WorkMode.PLAYER,
|
||||
};
|
||||
this.users.set(id, user);
|
||||
this.certs.set(id, { status: CertStatus.NONE });
|
||||
this.certs.set(id, {
|
||||
userId: id,
|
||||
status: CertStatus.NONE,
|
||||
appliedAt: new Date(),
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
registerAdmin(params: { openid: string; nickname: string }): UserAccount {
|
||||
return this.registerUser({
|
||||
openid: params.openid,
|
||||
nickname: params.nickname,
|
||||
initialRoles: UserRoleFlag.PLAYER | UserRoleFlag.ADMIN,
|
||||
});
|
||||
}
|
||||
|
||||
getUser(userId: number): UserAccount {
|
||||
const user = this.users.get(userId);
|
||||
if (!user) throw new Error('用户不存在');
|
||||
return user;
|
||||
}
|
||||
|
||||
hasRole(userId: number, role: UserRoleFlag): boolean {
|
||||
const user = this.users.get(userId);
|
||||
if (!user) return false;
|
||||
return (user.roleFlags & role) === role;
|
||||
}
|
||||
|
||||
grantRole(userId: number, role: UserRoleFlag): void {
|
||||
const user = this.getUser(userId);
|
||||
user.roleFlags |= role;
|
||||
}
|
||||
|
||||
revokeRole(userId: number, role: UserRoleFlag): void {
|
||||
const user = this.getUser(userId);
|
||||
user.roleFlags &= ~role;
|
||||
}
|
||||
|
||||
isPlayer(userId: number): boolean {
|
||||
return this.hasRole(userId, UserRoleFlag.PLAYER);
|
||||
}
|
||||
|
||||
isExpert(userId: number): boolean {
|
||||
return this.hasRole(userId, UserRoleFlag.EXPERT);
|
||||
}
|
||||
|
||||
isHost(userId: number): boolean {
|
||||
return this.hasRole(userId, UserRoleFlag.HOST);
|
||||
}
|
||||
|
||||
isAdmin(userId: number): boolean {
|
||||
return this.hasRole(userId, UserRoleFlag.ADMIN);
|
||||
}
|
||||
|
||||
getCertStatus(userId: number): CertStatus {
|
||||
return this.certs.get(userId)?.status ?? CertStatus.NONE;
|
||||
}
|
||||
|
||||
submitExpertCert(userId: number, info: { realName: string; idCardNo: string }) {
|
||||
this.certs.set(userId, {
|
||||
status: CertStatus.PENDING,
|
||||
realName: info.realName,
|
||||
idCardNo: info.idCardNo,
|
||||
});
|
||||
getCertRecord(userId: number): ExpertCertRecord | undefined {
|
||||
return this.certs.get(userId);
|
||||
}
|
||||
|
||||
approveExpertCert(userId: number) {
|
||||
submitExpertCert(userId: number, info: { realName: string; idCardNo: string }): ExpertCertRecord {
|
||||
if (!info.realName || !info.realName.trim()) {
|
||||
throw new Error('真实姓名不能为空');
|
||||
}
|
||||
if (!info.idCardNo || !info.idCardNo.trim()) {
|
||||
throw new Error('身份证号不能为空');
|
||||
}
|
||||
const record: ExpertCertRecord = {
|
||||
userId,
|
||||
status: CertStatus.PENDING,
|
||||
realName: info.realName.trim(),
|
||||
idCardNo: info.idCardNo.trim(),
|
||||
appliedAt: new Date(),
|
||||
};
|
||||
this.certs.set(userId, record);
|
||||
return record;
|
||||
}
|
||||
|
||||
approveExpertCert(userId: number, operatorAdminId?: number): ExpertCertRecord {
|
||||
if (operatorAdminId !== undefined && !this.isAdmin(operatorAdminId)) {
|
||||
throw new Error('只有平台管理员有权审核大神认证申请');
|
||||
}
|
||||
const cert = this.certs.get(userId);
|
||||
if (!cert) throw new Error('未找到认证申请');
|
||||
cert.status = CertStatus.APPROVED;
|
||||
cert.auditedAt = new Date();
|
||||
if (operatorAdminId !== undefined) {
|
||||
cert.auditedBy = operatorAdminId;
|
||||
}
|
||||
|
||||
const user = this.getUser(userId);
|
||||
user.roleFlags |= UserRoleFlag.EXPERT;
|
||||
return cert;
|
||||
}
|
||||
|
||||
switchMode(userId: number, targetMode: WorkMode) {
|
||||
rejectExpertCert(userId: number, reason: string, operatorAdminId?: number): ExpertCertRecord {
|
||||
if (operatorAdminId !== undefined && !this.isAdmin(operatorAdminId)) {
|
||||
throw new Error('只有平台管理员有权审核大神认证申请');
|
||||
}
|
||||
const cert = this.certs.get(userId);
|
||||
if (!cert) throw new Error('未找到认证申请');
|
||||
cert.status = CertStatus.REJECTED;
|
||||
cert.rejectReason = reason;
|
||||
cert.auditedAt = new Date();
|
||||
if (operatorAdminId !== undefined) {
|
||||
cert.auditedBy = operatorAdminId;
|
||||
}
|
||||
return cert;
|
||||
}
|
||||
|
||||
switchMode(userId: number, targetMode: WorkMode): void {
|
||||
const user = this.getUser(userId);
|
||||
if (targetMode === WorkMode.EXPERT && !(user.roleFlags & UserRoleFlag.EXPERT)) {
|
||||
throw new Error('尚未开通或通过大神认证,无法切换至接单模式');
|
||||
}
|
||||
user.currentMode = targetMode;
|
||||
}
|
||||
|
||||
checkCanClaimOrder(userId: number): void {
|
||||
const user = this.getUser(userId);
|
||||
if (!(user.roleFlags & UserRoleFlag.EXPERT) || this.getCertStatus(userId) !== CertStatus.APPROVED) {
|
||||
throw new Error('未认证大神无法接单');
|
||||
}
|
||||
}
|
||||
|
||||
checkCanAudit(operatorId: number): void {
|
||||
if (!this.isAdmin(operatorId)) {
|
||||
throw new Error('只有平台管理员有权审核大神认证申请');
|
||||
}
|
||||
}
|
||||
|
||||
checkCanArbitrate(operatorId: number): void {
|
||||
if (!this.isAdmin(operatorId)) {
|
||||
throw new Error('只有平台管理员有权执行争议仲裁');
|
||||
}
|
||||
}
|
||||
|
||||
checkCanAdjustPrice(operatorId: number, creatorId: number): void {
|
||||
if (operatorId !== creatorId && !this.isAdmin(operatorId)) {
|
||||
throw new Error('只有发起人有权调整活动出价');
|
||||
}
|
||||
}
|
||||
|
||||
checkCanDissolveActivity(operatorId: number, creatorId: number): void {
|
||||
if (operatorId !== creatorId && !this.isAdmin(operatorId)) {
|
||||
throw new Error('只有发起人有权解散此组局');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { OrderStatus } from './order-state-machine';
|
||||
import { CompanionTrack, ExpenseBurdenType } from '../companion/service-category';
|
||||
import { AuthRoleManager, CertStatus } from '../auth/auth-role.manager';
|
||||
|
||||
export interface GameDetails {
|
||||
gameName: string;
|
||||
@@ -38,6 +39,13 @@ export interface CompanionOrder {
|
||||
offlineDetails?: OfflineDetails;
|
||||
offlineVerifyCode?: string;
|
||||
checkedInAt?: Date;
|
||||
completedAt?: Date;
|
||||
disputeReason?: string;
|
||||
disputedAt?: Date;
|
||||
arbitratedAt?: Date;
|
||||
arbitratedBy?: number;
|
||||
arbitrationDecision?: 'REFUND' | 'RELEASE';
|
||||
arbitrationReason?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@@ -75,9 +83,12 @@ export class CompanionOrderService {
|
||||
return order;
|
||||
}
|
||||
|
||||
payOrder(orderId: string): CompanionOrder {
|
||||
payOrder(orderId: string, buyerId?: number): CompanionOrder {
|
||||
const order = this.orders.get(orderId);
|
||||
if (!order) throw new Error('订单不存在');
|
||||
if (buyerId !== undefined && order.buyerId !== buyerId) {
|
||||
throw new Error('非下单买家无权支付该订单');
|
||||
}
|
||||
if (order.status !== OrderStatus.PENDING_PAYMENT) {
|
||||
throw new Error(`订单状态 [${order.status}] 不允许支付`);
|
||||
}
|
||||
@@ -86,9 +97,30 @@ export class CompanionOrderService {
|
||||
return order;
|
||||
}
|
||||
|
||||
expertAccept(orderId: string, expertId: number): CompanionOrder {
|
||||
buyerCancel(orderId: string, buyerId: number): CompanionOrder {
|
||||
const order = this.orders.get(orderId);
|
||||
if (!order) throw new Error('订单不存在');
|
||||
if (order.buyerId !== buyerId) {
|
||||
throw new Error('只有下单买家有权取消订单');
|
||||
}
|
||||
if (order.status !== OrderStatus.PENDING_PAYMENT) {
|
||||
throw new Error(`订单当前状态 [${order.status}] 不可取消`);
|
||||
}
|
||||
|
||||
order.status = OrderStatus.CANCELLED;
|
||||
return order;
|
||||
}
|
||||
|
||||
expertAccept(orderId: string, expertId: number, roleManager?: AuthRoleManager): CompanionOrder {
|
||||
const order = this.orders.get(orderId);
|
||||
if (!order) throw new Error('订单不存在');
|
||||
|
||||
if (roleManager) {
|
||||
if (!roleManager.isExpert(expertId) || roleManager.getCertStatus(expertId) !== CertStatus.APPROVED) {
|
||||
throw new Error('未认证大神无法接单');
|
||||
}
|
||||
}
|
||||
|
||||
if (order.expertId !== expertId) throw new Error('非接单大神无权操作');
|
||||
if (order.status !== OrderStatus.PENDING_ACCEPT) {
|
||||
throw new Error(`订单当前状态 [${order.status}] 不可接单`);
|
||||
@@ -106,6 +138,20 @@ export class CompanionOrderService {
|
||||
return order;
|
||||
}
|
||||
|
||||
expertReject(orderId: string, expertId: number, reason?: string): CompanionOrder {
|
||||
const order = this.orders.get(orderId);
|
||||
if (!order) throw new Error('订单不存在');
|
||||
if (order.expertId !== expertId) {
|
||||
throw new Error('只有指定接单大神有权拒单');
|
||||
}
|
||||
if (order.status !== OrderStatus.PENDING_ACCEPT) {
|
||||
throw new Error(`订单当前状态 [${order.status}] 不可拒单`);
|
||||
}
|
||||
|
||||
order.status = OrderStatus.REFUNDED;
|
||||
return order;
|
||||
}
|
||||
|
||||
verifyOfflineCheckin(params: {
|
||||
orderId: string;
|
||||
operatorExpertId: number;
|
||||
@@ -131,4 +177,99 @@ export class CompanionOrderService {
|
||||
|
||||
return { success: true, order };
|
||||
}
|
||||
|
||||
expertApplyFinish(orderId: string, expertId: number): CompanionOrder {
|
||||
const order = this.orders.get(orderId);
|
||||
if (!order) throw new Error('订单不存在');
|
||||
if (order.expertId !== expertId) {
|
||||
throw new Error('只有接单大神有权申请完工结算');
|
||||
}
|
||||
if (order.status !== OrderStatus.IN_SERVICE) {
|
||||
throw new Error(`订单当前状态 [${order.status}] 无法申请完工结算`);
|
||||
}
|
||||
|
||||
order.status = OrderStatus.PENDING_CONFIRM;
|
||||
return order;
|
||||
}
|
||||
|
||||
buyerConfirm(orderId: string, buyerId: number): CompanionOrder {
|
||||
const order = this.orders.get(orderId);
|
||||
if (!order) throw new Error('订单不存在');
|
||||
if (order.buyerId !== buyerId) {
|
||||
throw new Error('只有下单买家有权确认完成验收');
|
||||
}
|
||||
if (order.status !== OrderStatus.PENDING_CONFIRM) {
|
||||
throw new Error(`订单当前状态 [${order.status}] 无法确认完成`);
|
||||
}
|
||||
|
||||
order.completedAt = new Date();
|
||||
order.status = OrderStatus.COMPLETED;
|
||||
return order;
|
||||
}
|
||||
|
||||
buyerDispute(orderId: string, buyerId: number, reason: string): CompanionOrder {
|
||||
const order = this.orders.get(orderId);
|
||||
if (!order) throw new Error('订单不存在');
|
||||
if (order.buyerId !== buyerId) {
|
||||
throw new Error('只有下单买家有权发起争议仲裁');
|
||||
}
|
||||
if (order.status !== OrderStatus.PENDING_CONFIRM) {
|
||||
throw new Error(`订单当前状态 [${order.status}] 无法发起争议仲裁`);
|
||||
}
|
||||
|
||||
order.disputeReason = reason;
|
||||
order.disputedAt = new Date();
|
||||
order.status = OrderStatus.DISPUTED;
|
||||
return order;
|
||||
}
|
||||
|
||||
arbitrate(params: {
|
||||
orderId: string;
|
||||
operatorAdminId: number;
|
||||
decision: 'REFUND' | 'RELEASE';
|
||||
reason?: string;
|
||||
roleManager?: AuthRoleManager;
|
||||
}): CompanionOrder {
|
||||
const order = this.orders.get(params.orderId);
|
||||
if (!order) throw new Error('订单不存在');
|
||||
|
||||
if (params.roleManager) {
|
||||
if (!params.roleManager.isAdmin(params.operatorAdminId)) {
|
||||
if (params.operatorAdminId === order.buyerId || params.operatorAdminId === order.expertId) {
|
||||
throw new Error('买家与大神不可越权执行管理员仲裁');
|
||||
}
|
||||
throw new Error('只有平台管理员有权执行争议仲裁');
|
||||
}
|
||||
} else {
|
||||
if (params.operatorAdminId === order.buyerId || params.operatorAdminId === order.expertId) {
|
||||
throw new Error('买家与大神不可越权执行管理员仲裁');
|
||||
}
|
||||
}
|
||||
|
||||
if (order.status !== OrderStatus.DISPUTED) {
|
||||
throw new Error(`订单当前状态 [${order.status}] 非争议仲裁中,不可执行仲裁`);
|
||||
}
|
||||
|
||||
order.arbitratedAt = new Date();
|
||||
order.arbitratedBy = params.operatorAdminId;
|
||||
order.arbitrationDecision = params.decision;
|
||||
order.arbitrationReason = params.reason;
|
||||
|
||||
if (params.decision === 'REFUND') {
|
||||
order.status = OrderStatus.REFUNDED;
|
||||
} else {
|
||||
order.status = OrderStatus.COMPLETED;
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
getAllOrders(): CompanionOrder[] {
|
||||
return Array.from(this.orders.values());
|
||||
}
|
||||
|
||||
getOrder(orderId: string): CompanionOrder | undefined {
|
||||
return this.orders.get(orderId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { OrderStatus } from '../order/order-state-machine';
|
||||
|
||||
export interface OrderReviewInput {
|
||||
orderId: string;
|
||||
orderStatus: OrderStatus;
|
||||
expertId: number;
|
||||
userId: number;
|
||||
userNickname: string;
|
||||
rating: number;
|
||||
tags?: string[];
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface ReviewRecord {
|
||||
id: string;
|
||||
orderId: string;
|
||||
expertId: number;
|
||||
userId: number;
|
||||
userNickname: string;
|
||||
rating: number;
|
||||
tags: string[];
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface PostCommentInput {
|
||||
postId: string;
|
||||
userId: number;
|
||||
author: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface CommentRecord {
|
||||
id: string;
|
||||
postId: string;
|
||||
userId: number;
|
||||
author: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export class ReviewAndCommentService {
|
||||
private reviews: ReviewRecord[] = [];
|
||||
private comments: CommentRecord[] = [];
|
||||
|
||||
submitOrderReview(input: OrderReviewInput): ReviewRecord {
|
||||
if (input.orderStatus !== OrderStatus.COMPLETED) {
|
||||
throw new Error('只有已完成(COMPLETED)状态的订单才允许提交服务评价');
|
||||
}
|
||||
|
||||
if (!input.rating || input.rating < 1 || input.rating > 5) {
|
||||
throw new Error('评分必须在 1 至 5 星之间');
|
||||
}
|
||||
|
||||
const exists = this.reviews.some(r => r.orderId === input.orderId);
|
||||
if (exists) {
|
||||
throw new Error('该订单已评价,不可重复提交评价');
|
||||
}
|
||||
|
||||
const review: ReviewRecord = {
|
||||
id: `rev_${Date.now()}_${Math.floor(100 + Math.random() * 900)}`,
|
||||
orderId: input.orderId,
|
||||
expertId: input.expertId,
|
||||
userId: input.userId,
|
||||
userNickname: input.userNickname || '买家用户',
|
||||
rating: Math.floor(input.rating),
|
||||
tags: input.tags || [],
|
||||
content: input.content || '好评!',
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.reviews.push(review);
|
||||
return review;
|
||||
}
|
||||
|
||||
getReviewsByExpert(expertId: number): ReviewRecord[] {
|
||||
return this.reviews.filter(r => r.expertId === expertId);
|
||||
}
|
||||
|
||||
calculateExpertAverageRating(expertId: number, baseRating: number = 5.0): number {
|
||||
const list = this.getReviewsByExpert(expertId);
|
||||
if (list.length === 0) return baseRating;
|
||||
const total = list.reduce((sum, item) => sum + item.rating, 0);
|
||||
return Number((total / list.length).toFixed(2));
|
||||
}
|
||||
|
||||
submitPostComment(input: PostCommentInput): CommentRecord {
|
||||
if (!input.content || !input.content.trim()) {
|
||||
throw new Error('评论内容不能为空');
|
||||
}
|
||||
if (input.content.trim().length > 500) {
|
||||
throw new Error('评论内容不能超过 500 个字符');
|
||||
}
|
||||
|
||||
const comment: CommentRecord = {
|
||||
id: `cmt_${Date.now()}_${Math.floor(100 + Math.random() * 900)}`,
|
||||
postId: input.postId,
|
||||
userId: input.userId,
|
||||
author: input.author || '热心搭子',
|
||||
content: input.content.trim(),
|
||||
createdAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.comments.push(comment);
|
||||
return comment;
|
||||
}
|
||||
|
||||
getCommentsByPost(postId: string): CommentRecord[] {
|
||||
return this.comments.filter(c => c.postId === postId);
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户