| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- 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<AuthSession | null>(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<AuthSession> {
- const session = this.localMode
- ? this.localRegister(input)
- : await this.call<AuthSession>('register', input);
- this.setSession(session);
- return session;
- }
- async login(identifier: string, password: string): Promise<AuthSession> {
- const session = this.localMode
- ? this.localLogin(identifier, password)
- : await this.call<AuthSession>('login', { identifier, password });
- this.setSession(session);
- return session;
- }
- async sendMobileCode(mobile: string): Promise<void> {
- 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<AuthSession> {
- 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<AuthSession> {
- 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<AppUser | null> {
- if (!this.session) return null;
- if (this.localMode) return this.session.user;
- const user = await this.call<AppUser>('me', this.authPayload());
- this.setSession({ ...this.session, user });
- return user;
- }
- logout(): void {
- localStorage.removeItem(SESSION_STORAGE_KEY);
- this.sessionSubject.next(null);
- }
- async getBalance(): Promise<CreditBalance> {
- if (this.localMode) return this.localBalance();
- return this.call<CreditBalance>('balance', this.authPayload());
- }
- async getLedger(limit = 50): Promise<CreditLedgerItem[]> {
- if (this.localMode) return this.localLedger().slice(0, limit);
- return this.call<CreditLedgerItem[]>('ledger', { ...this.authPayload(), limit });
- }
- async createRechargeOrder(amountCny: number): Promise<RechargeOrder> {
- throw new Error('试运营阶段暂未开放在线充值,请联系管理员调整额度');
- if (this.localMode) return this.localCreateRechargeOrder(amountCny);
- return this.call<RechargeOrder>('createRechargeOrder', { ...this.authPayload(), amountCny });
- }
- async reserveCredit(operation: string, cost: number, title: string, detail: any = {}): Promise<CreditReserveResult> {
- 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<CreditReserveResult>('reserve', { ...this.authPayload(), operation, cost, title, detail });
- }
- async commitReservation(reservationId: string, detail: any = {}): Promise<void> {
- if (!CREDIT_LIMIT_ENABLED || reservationId.startsWith('credit-disabled-')) return;
- if (this.localMode) return;
- await this.call<void>('commitReservation', { ...this.authPayload(), reservationId, detail });
- }
- async refundReservation(reservationId: string, reason: string): Promise<void> {
- if (!CREDIT_LIMIT_ENABLED || reservationId.startsWith('credit-disabled-')) return;
- if (this.localMode) {
- this.localRefund(reservationId, reason);
- return;
- }
- await this.call<void>('refundReservation', { ...this.authPayload(), reservationId, reason });
- }
- async adminListUsers(): Promise<AdminUserRow[]> {
- if (this.localMode) return this.localUsers().map((u: any) => ({ ...u.user, creditBalance: u.balance || 0 }));
- return this.call<AdminUserRow[]>('adminListUsers', this.authPayload());
- }
- async adminCreateUser(input: AdminCreateUserInput): Promise<AppUser> {
- if (this.localMode) return this.localAdminCreateUser(input);
- return this.call<AppUser>('adminCreateUser', { ...this.authPayload(), ...input });
- }
- async adminAdjustCredit(userId: string, amount: number, note: string): Promise<void> {
- 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<void>('adminAdjustCredit', { ...this.authPayload(), userId, amount, note });
- }
- async changePassword(oldPassword: string, newPassword: string): Promise<void> {
- 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<void>('changePassword', { ...this.authPayload(), oldPassword, newPassword });
- }
- private async call<T>(action: string, params: Record<string, any>): Promise<T> {
- const res = await this.parse.call<T>(CLOUD_FN.authCredit, { action, ...params });
- if (res.code !== 200 || !res.success) {
- throw new Error(res.error || '账号服务调用失败');
- }
- return res.data as T;
- }
- private authPayload(): Record<string, any> {
- 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<any> {
- 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;
- }
- }
|