feat: 完善对象存储上传与真实条码二维码引擎,彻底剥离保底Mock数据

这个提交包含在:
xcbyy
2026-10-09 17:18:14 +08:00
父节点 6d3076cdb8
当前提交 d0f4050cdc
共有 329 个文件被更改,包括 54416 次插入 和 926 次删除

查看文件

@@ -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);
}
}