import { Injectable } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; import { CLOUD_FN } from './cloud-functions'; import { ParseService } from './parse.service'; export type AppUserRole = 'user' | 'admin'; export interface AppUser { objectId: string; username: string; email?: string; phone?: string; displayName?: string; companyId?: string; role: AppUserRole; createdAt?: string; } export interface AuthSession { token: string; user: AppUser; } export interface CreditBalance { balance: number; gifted: number; totalRecharged: number; totalConsumed: number; } export interface CreditLedgerItem { objectId: string; userId?: string; type: 'gift' | 'recharge' | 'consume' | 'refund' | 'admin_adjust' | string; amount: number; balanceAfter: number; title: string; detail?: any; createdAt: string; } export interface RechargeOrder { objectId: string; amountCny: number; creditAmount: number; status: 'pending' | 'paid' | 'cancelled'; createdAt: string; } export interface AdminUserRow extends AppUser { creditBalance: number; status?: string; } export interface AdminCreateUserInput { username: string; password: string; email?: string; phone?: string; displayName?: string; role?: AppUserRole; initialCredits?: number; } export interface CreditReserveResult { reservationId: string; balance: number; cost: number; } const SESSION_STORAGE_KEY = 'videoWorkflow.authSession'; const LOCAL_USERS_KEY = 'videoWorkflow.localAuth.users'; const LOCAL_LEDGER_KEY = 'videoWorkflow.localAuth.ledger'; const LOCAL_ORDERS_KEY = 'videoWorkflow.localAuth.orders'; const PARSE_API_HOST = 'https://server.fmode.cn'; const PARSE_APP_ID = 'ncloudmaster'; const LOGIN_SMS_COMPANY = 'E4KpGvTEto'; const CREDIT_LIMIT_ENABLED = false; @Injectable({ providedIn: 'root' }) export class AuthCreditService { private readonly sessionSubject = new BehaviorSubject(this.readSession()); private readonly loginRequiredSubject = new Subject<{ featureName: string }>(); readonly session$ = this.sessionSubject.asObservable(); readonly loginRequired$ = this.loginRequiredSubject.asObservable(); readonly localMode = !CLOUD_FN.authCredit; constructor(private parse: ParseService) {} get session(): AuthSession | null { return this.sessionSubject.value; } get currentUser(): AppUser | null { return this.session?.user || null; } get isLoggedIn(): boolean { return !!this.session?.token; } get isAdmin(): boolean { return this.currentUser?.role === 'admin'; } requestLogin(featureName = '该功能'): void { this.loginRequiredSubject.next({ featureName }); } async register(input: { username: string; password: string; email?: string; phone?: string; displayName?: string; }): Promise { const session = this.localMode ? this.localRegister(input) : await this.call('register', input); this.setSession(session); return session; } async login(identifier: string, password: string): Promise { const session = this.localMode ? this.localLogin(identifier, password) : await this.call('login', { identifier, password }); this.setSession(session); return session; } async sendMobileCode(mobile: string): Promise { const normalized = this.normalizeMobile(mobile); if (!normalized) throw new Error('请输入正确的手机号'); const resp = await fetch(`${PARSE_API_HOST}/api/apig/message`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': PARSE_APP_ID, }, body: JSON.stringify({ mobile: normalized, company: LOGIN_SMS_COMPANY }), }); const result = await resp.json().catch(() => ({})); if (!resp.ok || result.code !== 1) { throw new Error(result.data?.msg || result.message || result.error || '验证码发送失败,请稍后重试'); } } async loginWithMobileCode(mobile: string, code: string): Promise { const normalized = this.normalizeMobile(mobile); const smsCode = String(code || '').trim(); if (!normalized) throw new Error('请输入正确的手机号'); if (smsCode.length < 4) throw new Error('请输入验证码'); const loginUrl = `${PARSE_API_HOST}/api/fmode/mobile?mobile=${encodeURIComponent(normalized)}&code=${encodeURIComponent(smsCode)}`; const resp = await fetch(loginUrl, { method: 'GET', headers: { 'X-Parse-Application-Id': PARSE_APP_ID }, }); const result = await resp.json().catch(() => ({})); const token = result?.data?.token; if (!resp.ok || result.code !== 200 || !token) { throw new Error(result.mess || result.message || result.error || '验证码错误或已过期'); } const parseUser = await this.fetchParseMe(token); const session: AuthSession = { token, user: this.parseUserToAppUser(parseUser, normalized), }; if (this.localMode) this.ensureLocalCreditUser(session.user); this.setSession(session); return session; } async loginWithParsePassword(username: string, password: string): Promise { const account = String(username || '').trim(); const pass = String(password || ''); if (!account || !pass) throw new Error('请输入账号和密码'); const params = new URLSearchParams({ username: account, password: pass }); const resp = await fetch(`${PARSE_API_HOST}/parse/login?${params.toString()}`, { method: 'GET', headers: { 'X-Parse-Application-Id': PARSE_APP_ID }, }); const loginData = await resp.json().catch(() => ({})); const token = loginData.sessionToken; if (!resp.ok || !token) { throw new Error(loginData.error || loginData.message || '账号或密码不正确'); } const parseUser = await this.fetchParseMe(token); const session: AuthSession = { token, user: this.parseUserToAppUser(parseUser, parseUser.mobilePhoneNumber || ''), }; if (this.localMode) this.ensureLocalCreditUser(session.user); this.setSession(session); return session; } async refreshMe(): Promise { if (!this.session) return null; if (this.localMode) return this.session.user; const user = await this.call('me', this.authPayload()); this.setSession({ ...this.session, user }); return user; } logout(): void { localStorage.removeItem(SESSION_STORAGE_KEY); this.sessionSubject.next(null); } async getBalance(): Promise { if (this.localMode) return this.localBalance(); return this.call('balance', this.authPayload()); } async getLedger(limit = 50): Promise { if (this.localMode) return this.localLedger().slice(0, limit); return this.call('ledger', { ...this.authPayload(), limit }); } async createRechargeOrder(amountCny: number): Promise { throw new Error('试运营阶段暂未开放在线充值,请联系管理员调整额度'); if (this.localMode) return this.localCreateRechargeOrder(amountCny); return this.call('createRechargeOrder', { ...this.authPayload(), amountCny }); } async reserveCredit(operation: string, cost: number, title: string, detail: any = {}): Promise { if (!CREDIT_LIMIT_ENABLED) { const balance = this.currentUser ? (await this.getBalance().catch(() => null))?.balance || 0 : 0; return { reservationId: `credit-disabled-${Date.now()}`, balance, cost: 0 }; } if (this.localMode) return this.localReserve(operation, cost, title, detail); return this.call('reserve', { ...this.authPayload(), operation, cost, title, detail }); } async commitReservation(reservationId: string, detail: any = {}): Promise { if (!CREDIT_LIMIT_ENABLED || reservationId.startsWith('credit-disabled-')) return; if (this.localMode) return; await this.call('commitReservation', { ...this.authPayload(), reservationId, detail }); } async refundReservation(reservationId: string, reason: string): Promise { if (!CREDIT_LIMIT_ENABLED || reservationId.startsWith('credit-disabled-')) return; if (this.localMode) { this.localRefund(reservationId, reason); return; } await this.call('refundReservation', { ...this.authPayload(), reservationId, reason }); } async adminListUsers(): Promise { if (this.localMode) return this.localUsers().map((u: any) => ({ ...u.user, creditBalance: u.balance || 0 })); return this.call('adminListUsers', this.authPayload()); } async adminCreateUser(input: AdminCreateUserInput): Promise { if (this.localMode) return this.localAdminCreateUser(input); return this.call('adminCreateUser', { ...this.authPayload(), ...input }); } async adminAdjustCredit(userId: string, amount: number, note: string): Promise { if (this.localMode) { const users = this.localUsers(); const row = users.find((u: any) => u.user.objectId === userId); if (!row) throw new Error('未找到用户'); row.balance = Math.max(0, Number(row.balance || 0) + Number(amount || 0)); this.writeLocalUsers(users); this.appendLocalLedger({ objectId: this.localId(), userId, type: 'admin_adjust', amount, balanceAfter: row.balance, title: note || '管理员调整', createdAt: new Date().toISOString(), }); return; } await this.call('adminAdjustCredit', { ...this.authPayload(), userId, amount, note }); } async changePassword(oldPassword: string, newPassword: string): Promise { if (this.localMode) { const session = this.session; if (!session) throw new Error('请先登录'); const users = this.localUsers(); const row = users.find((u: any) => u.user.objectId === session.user.objectId); if (!row || row.password !== oldPassword) throw new Error('原密码不正确'); row.password = newPassword; this.writeLocalUsers(users); return; } await this.call('changePassword', { ...this.authPayload(), oldPassword, newPassword }); } private async call(action: string, params: Record): Promise { const res = await this.parse.call(CLOUD_FN.authCredit, { action, ...params }); if (res.code !== 200 || !res.success) { throw new Error(res.error || '账号服务调用失败'); } return res.data as T; } private authPayload(): Record { if (!this.session) throw new Error('请先登录'); return { sessionToken: this.session.token, userId: this.session.user.objectId }; } private readSession(): AuthSession | null { try { const raw = localStorage.getItem(SESSION_STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } private setSession(session: AuthSession): void { localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session)); this.sessionSubject.next(session); } private normalizeMobile(mobile: string): string { const value = String(mobile || '').replace(/\D/g, '').slice(0, 11); return /^1[3-9]\d{9}$/.test(value) ? value : ''; } private async fetchParseMe(sessionToken: string): Promise { const resp = await fetch(`${PARSE_API_HOST}/parse/users/me?include=company`, { method: 'GET', headers: { 'X-Parse-Application-Id': PARSE_APP_ID, 'X-Parse-Session-Token': sessionToken, }, }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.objectId) { throw new Error(data.error || data.message || '登录成功但无法获取用户信息'); } return data; } private parseUserToAppUser(parseUser: any, mobile: string): AppUser { const displayName = parseUser.nickname || parseUser.name || parseUser.displayName || this.maskMobile(parseUser.mobilePhoneNumber || mobile) || parseUser.username || parseUser.objectId; return { objectId: parseUser.objectId, username: parseUser.username || parseUser.mobilePhoneNumber || mobile || parseUser.objectId, email: parseUser.email || '', phone: parseUser.mobilePhoneNumber || mobile || '', displayName, companyId: parseUser.company?.objectId || '', role: parseUser.role === 'admin' || parseUser.isAdmin === true ? 'admin' : 'user', createdAt: parseUser.createdAt, }; } private maskMobile(mobile: string): string { const value = String(mobile || ''); return /^1[3-9]\d{9}$/.test(value) ? value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : value; } private ensureLocalCreditUser(user: AppUser): void { const users = this.localUsers(); const row = users.find((u: any) => u.user.objectId === user.objectId); if (row) { row.user = { ...row.user, ...user }; this.writeLocalUsers(users); return; } users.push({ user, password: '', balance: 10 }); this.writeLocalUsers(users); this.appendLocalLedger({ objectId: this.localId(), userId: user.objectId, type: 'gift', amount: 10, balanceAfter: 10, title: '初始赠送积分', createdAt: new Date().toISOString(), }); } private localRegister(input: any): AuthSession { const users = this.localUsers(); const identifier = String(input.username || '').trim(); if (!identifier || !input.password) throw new Error('请输入账号和密码'); const exists = users.some((u: any) => u.user.username === identifier || (input.email && u.user.email === input.email) || (input.phone && u.user.phone === input.phone) ); if (exists) throw new Error('账号已存在'); const isFirst = users.length === 0; const user: AppUser = { objectId: this.localId(), username: identifier, email: input.email || '', phone: input.phone || '', displayName: input.displayName || identifier, role: isFirst ? 'admin' : 'user', createdAt: new Date().toISOString(), }; users.push({ user, password: input.password, balance: 10 }); this.writeLocalUsers(users); this.appendLocalLedger({ objectId: this.localId(), userId: user.objectId, type: 'gift', amount: 10, balanceAfter: 10, title: '注册赠送积分', createdAt: new Date().toISOString(), }); return { token: this.localId() + this.localId(), user }; } private localLogin(identifier: string, password: string): AuthSession { const id = String(identifier || '').trim(); const row = this.localUsers().find((u: any) => (u.user.username === id || u.user.email === id || u.user.phone === id) && u.password === password ); if (!row) throw new Error('账号或密码不正确'); return { token: this.localId() + this.localId(), user: row.user }; } private localBalance(): CreditBalance { const userId = this.currentUser?.objectId; const row = this.localUsers().find((u: any) => u.user.objectId === userId); const ledger = this.localLedger().filter((i: any) => i.userId === userId); return { balance: Number(row?.balance || 0), gifted: ledger.filter((i) => i.type === 'gift').reduce((s, i) => s + Number(i.amount || 0), 0), totalRecharged: ledger.filter((i) => i.type === 'recharge').reduce((s, i) => s + Number(i.amount || 0), 0), totalConsumed: Math.abs(ledger.filter((i) => i.type === 'consume').reduce((s, i) => s + Number(i.amount || 0), 0)), }; } private localAdminCreateUser(input: AdminCreateUserInput): AppUser { if (!this.isAdmin) throw new Error('没有管理员权限'); const username = String(input.username || '').trim(); const password = String(input.password || '').trim(); if (!username || !password) throw new Error('请输入用户名和初始密码'); const users = this.localUsers(); const exists = users.some((u: any) => u.user.username === username || (input.email && u.user.email === input.email) || (input.phone && u.user.phone === input.phone) ); if (exists) throw new Error('账号已存在'); const initialCredits = Math.max(0, Number(input.initialCredits ?? 10)); const user: AppUser = { objectId: this.localId(), username, email: input.email || '', phone: input.phone || '', displayName: input.displayName || username, role: input.role || 'user', createdAt: new Date().toISOString(), }; users.push({ user, password, balance: initialCredits }); this.writeLocalUsers(users); if (initialCredits > 0) { this.appendLocalLedger({ objectId: this.localId(), userId: user.objectId, type: 'admin_adjust', amount: initialCredits, balanceAfter: initialCredits, title: '管理员发放初始积分', createdAt: new Date().toISOString(), }); } return user; } private localCreateRechargeOrder(amountCny: number): RechargeOrder { if (!this.currentUser) throw new Error('请先登录'); const order: RechargeOrder = { objectId: this.localId(), amountCny, creditAmount: Math.round(Number(amountCny || 0) * 10), status: 'pending', createdAt: new Date().toISOString(), }; const orders = this.localOrders(); orders.unshift({ ...order, userId: this.currentUser.objectId }); localStorage.setItem(LOCAL_ORDERS_KEY, JSON.stringify(orders)); return order; } private localReserve(operation: string, cost: number, title: string, detail: any): CreditReserveResult { if (!this.currentUser) throw new Error('请先登录'); const users = this.localUsers(); const row = users.find((u: any) => u.user.objectId === this.currentUser!.objectId); const amount = Math.max(0, Number(cost || 0)); if (!row || Number(row.balance || 0) < amount) throw new Error('当前额度不足,请联系管理员调整额度'); row.balance = Number(row.balance || 0) - amount; this.writeLocalUsers(users); const reservationId = this.localId(); this.appendLocalLedger({ objectId: reservationId, userId: this.currentUser.objectId, type: 'consume', amount: -amount, balanceAfter: row.balance, title, detail: { operation, ...detail, status: 'reserved' }, createdAt: new Date().toISOString(), }); return { reservationId, balance: row.balance, cost: amount }; } private localRefund(reservationId: string, reason: string): void { const ledger = this.localLedger(); const item = ledger.find((i: any) => i.objectId === reservationId && i.type === 'consume'); if (!item || item.detail?.refunded) return; const users = this.localUsers(); const row = users.find((u: any) => u.user.objectId === item.userId); const refund = Math.abs(Number(item.amount || 0)); if (!row) return; row.balance = Number(row.balance || 0) + refund; item.detail = { ...(item.detail || {}), refunded: true, refundReason: reason }; this.writeLocalUsers(users); localStorage.setItem(LOCAL_LEDGER_KEY, JSON.stringify(ledger)); this.appendLocalLedger({ objectId: this.localId(), userId: item.userId, type: 'refund', amount: refund, balanceAfter: row.balance, title: reason || '任务失败退回积分', createdAt: new Date().toISOString(), }); } private localUsers(): any[] { try { return JSON.parse(localStorage.getItem(LOCAL_USERS_KEY) || '[]'); } catch { return []; } } private writeLocalUsers(users: any[]): void { localStorage.setItem(LOCAL_USERS_KEY, JSON.stringify(users)); } private localLedger(): CreditLedgerItem[] { const userId = this.currentUser?.objectId; try { const rows = JSON.parse(localStorage.getItem(LOCAL_LEDGER_KEY) || '[]'); return rows.filter((i: any) => !userId || i.userId === userId); } catch { return []; } } private appendLocalLedger(item: any): void { const rows = JSON.parse(localStorage.getItem(LOCAL_LEDGER_KEY) || '[]'); rows.unshift(item); localStorage.setItem(LOCAL_LEDGER_KEY, JSON.stringify(rows)); } private localOrders(): any[] { try { return JSON.parse(localStorage.getItem(LOCAL_ORDERS_KEY) || '[]'); } catch { return []; } } private localId(): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let s = ''; for (let i = 0; i < 10; i++) s += chars.charAt(Math.floor(Math.random() * chars.length)); return s; } }