""" 管理员 API 蓝图:用户管理、生成历史、版本管理(后台) """ import json import os import re import requests from flask import Blueprint, request, jsonify, session import pymysql from werkzeug.security import generate_password_hash from utils.db import get_db from utils.auth import admin_required, get_current_admin_role ALLOWED_DEDUPE_TOTAL_DATA_ADMIN_USERNAMES = {'刘丽泓'} def _can_access_dedupe_total_data(role, current_row): if role == 'super_admin': return True if role != 'admin' or not current_row: return False return (current_row.get('username') or '').strip() in ALLOWED_DEDUPE_TOTAL_DATA_ADMIN_USERNAMES def _ensure_dedupe_total_data_access(): role, current_row = get_current_admin_role() if not _can_access_dedupe_total_data(role, current_row): return None, None, (jsonify({'success': False, 'error': '无权限访问数据去重总数据'}), 403) return role, current_row, None from ali_oss import upload_file as oss_upload_file try: from config import bucket_path, backend_java_base_url except ImportError: bucket_path = os.environ.get('BUCKET_PATH', 'nanri-image/') backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/') admin_api = Blueprint('admin_api', __name__, url_prefix='/api/admin') def _safe_version_key(version): """将版本号转为安全的 OSS 对象名部分""" s = (version or '').strip() s = re.sub(r'[^\w.\-]', '_', s) return s or 'unknown' def _proxy_backend_java(method, path, *, params=None, json_data=None, files=None, data=None): url = f"{backend_java_base_url}{path}" try: resp = requests.request(method=method, url=url, params=params, json=json_data, files=files, data=data, timeout=10) except requests.RequestException: return None, jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502 try: data = resp.json() except ValueError: data = None if resp.status_code >= 400: if isinstance(data, dict): return data, jsonify({'success': False, 'error': data.get('message') or data.get('error') or 'backend-java 请求失败'}), resp.status_code return None, jsonify({'success': False, 'error': 'backend-java 请求失败'}), resp.status_code if not isinstance(data, dict): return None, jsonify({'success': False, 'error': 'backend-java 返回格式错误'}), 502 if not data.get('success'): return data, jsonify({'success': False, 'error': data.get('message') or '操作失败'}), 200 return data, None, 200 # ---------- 用户管理 ---------- @admin_api.route('/users') @admin_required def list_users(): """分页获取用户列表;支持用户名模糊搜索、指定管理员所属普通用户筛选""" role, current_row = get_current_admin_role() if not role: return jsonify({'success': False, 'error': '需要管理员权限'}), 403 page = max(1, int(request.args.get('page', 1))) page_size = min(50, max(5, int(request.args.get('page_size', 15)))) offset = (page - 1) * page_size search_username = (request.args.get('username') or request.args.get('search') or '').strip() created_by_id_arg = request.args.get('created_by_id') or request.args.get('admin_id') created_by_id = int(created_by_id_arg) if created_by_id_arg and str(created_by_id_arg).isdigit() else None if role != 'super_admin': created_by_id = None try: conn = get_db() with conn.cursor() as cur: if role == 'super_admin': where_parts = ["1=1"] params = [] if search_username: where_parts.append("u.username LIKE %s") params.append("%" + search_username + "%") if created_by_id is not None: where_parts.append("u.created_by_id = %s") params.append(created_by_id) where_sql = " AND ".join(where_parts) cur.execute( """SELECT u.id, u.username, u.is_admin, u.role, u.created_at, u.created_by_id, creator.username AS creator_username FROM users u LEFT JOIN users creator ON creator.id = u.created_by_id WHERE """ + where_sql + """ ORDER BY u.id LIMIT %s OFFSET %s""", tuple(params) + (page_size, offset), ) rows = cur.fetchall() cur.execute("SELECT COUNT(*) as total FROM users u WHERE " + where_sql, tuple(params)) total = cur.fetchone()['total'] cur.execute("SELECT id, username FROM users WHERE role = 'admin' ORDER BY id") admins = [{'id': r['id'], 'username': r['username']} for r in cur.fetchall()] else: admin_id = current_row['id'] where_parts = ["(u.id = %s OR (u.role = 'normal' AND u.created_by_id = %s))"] params = [admin_id, admin_id] if search_username: where_parts.append("u.username LIKE %s") params.append("%" + search_username + "%") where_sql = " AND ".join(where_parts) cur.execute( """SELECT u.id, u.username, u.is_admin, u.role, u.created_at, u.created_by_id, creator.username AS creator_username FROM users u LEFT JOIN users creator ON creator.id = u.created_by_id WHERE """ + where_sql + """ ORDER BY u.id LIMIT %s OFFSET %s""", tuple(params) + (page_size, offset), ) rows = cur.fetchall() cur.execute( "SELECT COUNT(*) as total FROM users u WHERE " + where_sql, tuple(params), ) total = cur.fetchone()['total'] admins = [] items = [ { 'id': r['id'], 'username': r['username'], 'is_admin': bool(r.get('is_admin')), 'role': r.get('role') or 'normal', 'created_by_id': r.get('created_by_id'), 'creator_username': r.get('creator_username') or '', 'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '', } for r in rows ] conn.close() return jsonify({ 'success': True, 'items': items, 'total': total, 'page': page, 'page_size': page_size, 'current_user_role': role, 'current_user_username': current_row.get('username') or '', 'admins': admins, }) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/user', methods=['POST']) @admin_required def create_user(): data = request.get_json() or {} username = (data.get('username') or '').strip() password = data.get('password') or '' role, current_row = get_current_admin_role() if not role: return jsonify({'success': False, 'error': '需要管理员权限'}), 403 want_role = (data.get('role') or 'normal').strip() or 'normal' if want_role not in ('admin', 'normal'): want_role = 'normal' if role == 'admin' and want_role == 'admin': return jsonify({'success': False, 'error': '仅超级管理员可创建管理员'}) if want_role == 'admin': want_created_by = current_row['id'] elif role == 'super_admin': want_created_by = data.get('created_by_id') else: want_created_by = current_row['id'] if not username or not password: return jsonify({'success': False, 'error': '用户名和密码不能为空'}) if len(username) < 2: return jsonify({'success': False, 'error': '用户名至少2个字符'}) if len(password) < 6: return jsonify({'success': False, 'error': '密码至少6个字符'}) if want_role == 'normal' and role == 'super_admin' and want_created_by is None: try: conn = get_db() with conn.cursor() as cur: cur.execute("SELECT id FROM users WHERE role = 'admin' ORDER BY id LIMIT 1") r = cur.fetchone() conn.close() want_created_by = r['id'] if r else current_row['id'] except Exception: want_created_by = current_row['id'] if want_role == 'normal' and want_created_by is None: want_created_by = current_row['id'] is_admin = 1 if want_role in ('super_admin', 'admin') else 0 pwd_hash = generate_password_hash(password, method='pbkdf2:sha256') column_ids = data.get('column_ids') if column_ids is None: column_ids = [] try: conn = get_db() with conn.cursor() as cur: cur.execute( "INSERT INTO users (username, password_hash, is_admin, role, created_by_id) VALUES (%s, %s, %s, %s, %s)", (username, pwd_hash, is_admin, want_role, want_created_by), ) new_uid = cur.lastrowid _set_user_column_permissions(cur, new_uid, column_ids) conn.commit() conn.close() return jsonify({'success': True, 'msg': '用户创建成功'}) except pymysql.IntegrityError: return jsonify({'success': False, 'error': '用户名已存在'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/user/', methods=['PUT']) @admin_required def update_user(uid): """更新用户(密码、角色)""" data = request.get_json() or {} password = data.get('password') want_role = (data.get('role') or '').strip() or data.get('role') role, current_row = get_current_admin_role() if not role: return jsonify({'success': False, 'error': '需要管理员权限'}), 403 if want_role is None and not password: return jsonify({'success': False, 'error': '请提供要修改的内容'}) try: conn = get_db() with conn.cursor() as cur: cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,)) target = cur.fetchone() if not target: conn.close() return jsonify({'success': False, 'error': '用户不存在'}) if role == 'admin': if target['role'] != 'normal' or target.get('created_by_id') != current_row['id']: conn.close() return jsonify({'success': False, 'error': '只能编辑自己创建的普通用户'}), 403 want_role = None else: if target.get('role') == 'super_admin': conn.close() return jsonify({'success': False, 'error': '不能修改超级管理员'}) if want_role == 'super_admin': return jsonify({'success': False, 'error': '不能将用户设为超级管理员'}) if want_role not in ('admin', 'normal', None, ''): want_role = None if password: if len(password) < 6: conn.close() return jsonify({'success': False, 'error': '密码至少6个字符'}) pwd_hash = generate_password_hash(password, method='pbkdf2:sha256') cur.execute("UPDATE users SET password_hash = %s WHERE id = %s", (pwd_hash, uid)) if want_role is not None and want_role != '': is_admin = 1 if want_role == 'admin' else 0 cur.execute( "UPDATE users SET is_admin = %s, role = %s WHERE id = %s", (is_admin, want_role, uid), ) column_ids = data.get('column_ids') if column_ids is not None: _set_user_column_permissions(cur, uid, column_ids) conn.commit() conn.close() return jsonify({'success': True, 'msg': '更新成功'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/user/', methods=['DELETE']) @admin_required def delete_user(uid): """删除用户""" if session.get('user_id') == uid: return jsonify({'success': False, 'error': '不能删除当前登录账号'}) role, current_row = get_current_admin_role() if not role: return jsonify({'success': False, 'error': '需要管理员权限'}), 403 try: conn = get_db() with conn.cursor() as cur: cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,)) target = cur.fetchone() if not target: conn.close() return jsonify({'success': False, 'error': '用户不存在'}) if target.get('role') == 'super_admin': conn.close() return jsonify({'success': False, 'error': '不能删除超级管理员'}) if role == 'admin': if target.get('role') != 'normal' or target.get('created_by_id') != current_row['id']: conn.close() return jsonify({'success': False, 'error': '只能删除自己创建的普通用户'}), 403 cur.execute("DELETE FROM users WHERE id = %s", (uid,)) affected = cur.rowcount conn.commit() conn.close() if affected == 0: return jsonify({'success': False, 'error': '用户不存在'}) return jsonify({'success': True, 'msg': '删除成功'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) # ---------- 生成历史 ---------- @admin_api.route('/history') @admin_required def history(): """管理员分页获取所有生成记录""" page = max(1, int(request.args.get('page', 1))) page_size = min(50, max(10, int(request.args.get('page_size', 15)))) offset = (page - 1) * page_size user_id = request.args.get('user_id', type=int) time_start = (request.args.get('time_start') or '').strip() time_end = (request.args.get('time_end') or '').strip() conditions, params = [], [] if user_id: conditions.append("h.user_id = %s") params.append(user_id) if time_start: conditions.append("h.created_at >= %s") params.append(time_start) if time_end: conditions.append("h.created_at <= %s") params.append(time_end + ' 23:59:59' if len(time_end) <= 10 else time_end) where_clause = " AND ".join(conditions) if conditions else "1=1" params_count = params[:] params.extend([page_size, offset]) try: conn = get_db() with conn.cursor() as cur: cur.execute( """SELECT h.id, h.user_id, h.created_at, h.panel_type, h.original_urls, h.params, h.result_urls, h.long_image_url, u.username FROM image_history h LEFT JOIN users u ON h.user_id = u.id WHERE """ + where_clause + """ ORDER BY h.created_at DESC LIMIT %s OFFSET %s""", params, ) rows = cur.fetchall() cur.execute("SELECT COUNT(*) as total FROM image_history h WHERE " + where_clause, params_count) total = cur.fetchone()['total'] conn.close() def _parse_json(val, default=None): if val is None: return default if default is not None else [] if isinstance(val, (list, dict)): return val try: return json.loads(val) except Exception: return default if default is not None else [] items = [] for r in rows: items.append({ 'id': r['id'], 'user_id': r['user_id'], 'username': r.get('username') or '-', 'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r['created_at'] else '', 'panel_type': r['panel_type'] or '', 'original_urls': _parse_json(r['original_urls'], []), 'params': _parse_json(r['params'], {}), 'result_urls': _parse_json(r['result_urls'], []), 'long_image_url': (r.get('long_image_url') or '').strip() or None, }) return jsonify({'success': True, 'items': items, 'total': total, 'page': page, 'page_size': page_size}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) # ---------- 栏目权限配置 ---------- @admin_api.route('/columns') @admin_required def list_columns(): """获取栏目列表(用于栏目配置与用户权限选择)""" try: conn = get_db() with conn.cursor() as cur: cur.execute( "SELECT id, name, column_key, created_at FROM columns ORDER BY id" ) rows = cur.fetchall() conn.close() items = [ { 'id': r['id'], 'name': r['name'] or '', 'column_key': r['column_key'] or '', 'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '', } for r in rows ] return jsonify({'success': True, 'items': items}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/column', methods=['POST']) @admin_required def create_column(): """新增栏目""" data = request.get_json() or {} name = (data.get('name') or '').strip() column_key = (data.get('column_key') or '').strip() if not name: return jsonify({'success': False, 'error': '栏目名不能为空'}) if not column_key: return jsonify({'success': False, 'error': '栏目标识不能为空'}) try: conn = get_db() with conn.cursor() as cur: cur.execute( "INSERT INTO columns (name, column_key) VALUES (%s, %s)", (name, column_key), ) cid = cur.lastrowid conn.commit() conn.close() return jsonify({'success': True, 'msg': '创建成功', 'id': cid}) except pymysql.IntegrityError: return jsonify({'success': False, 'error': '栏目标识已存在'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/column/', methods=['PUT']) @admin_required def update_column(cid): """更新栏目""" data = request.get_json() or {} name = (data.get('name') or '').strip() column_key = (data.get('column_key') or '').strip() if not name: return jsonify({'success': False, 'error': '栏目名不能为空'}) if not column_key: return jsonify({'success': False, 'error': '栏目标识不能为空'}) try: conn = get_db() with conn.cursor() as cur: cur.execute( "UPDATE columns SET name = %s, column_key = %s WHERE id = %s", (name, column_key, cid), ) if cur.rowcount == 0: conn.close() return jsonify({'success': False, 'error': '栏目不存在'}) conn.commit() conn.close() return jsonify({'success': True, 'msg': '更新成功'}) except pymysql.IntegrityError: return jsonify({'success': False, 'error': '栏目标识已存在'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/column/', methods=['DELETE']) @admin_required def delete_column(cid): """删除栏目(会同步删除用户栏目权限关联)""" try: conn = get_db() with conn.cursor() as cur: cur.execute("DELETE FROM user_column_permission WHERE column_id = %s", (cid,)) cur.execute("DELETE FROM columns WHERE id = %s", (cid,)) if cur.rowcount == 0: conn.close() return jsonify({'success': False, 'error': '栏目不存在'}) conn.commit() conn.close() return jsonify({'success': True, 'msg': '删除成功'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/user//columns') @admin_required def get_user_columns(uid): """获取某用户的栏目权限 ID 列表(编辑用户时回显)""" try: conn = get_db() with conn.cursor() as cur: cur.execute( "SELECT column_id FROM user_column_permission WHERE user_id = %s", (uid,), ) rows = cur.fetchall() conn.close() column_ids = [r['column_id'] for r in rows] return jsonify({'success': True, 'column_ids': column_ids}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/user//column-permissions') @admin_required def get_user_column_permissions(uid): """根据用户获取其拥有的栏目权限详细信息""" try: conn = get_db() with conn.cursor() as cur: cur.execute( """ SELECT c.id, c.name, c.column_key, c.created_at FROM user_column_permission ucp JOIN columns c ON c.id = ucp.column_id WHERE ucp.user_id = %s ORDER BY c.id """, (uid,), ) rows = cur.fetchall() conn.close() items = [ { 'id': r['id'], 'name': r['name'] or '', 'column_key': r['column_key'] or '', 'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '', } for r in rows ] return jsonify({'success': True, 'items': items}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) def _set_user_column_permissions(cur, user_id, column_ids): """设置用户栏目权限:先删后插。cur 为已打开的游标。""" cur.execute("DELETE FROM user_column_permission WHERE user_id = %s", (user_id,)) if column_ids: column_ids = [int(x) for x in column_ids if x] for cid in column_ids: cur.execute( "INSERT INTO user_column_permission (user_id, column_id) VALUES (%s, %s)", (user_id, cid), ) # ---------- 版本管理(web_config) ---------- @admin_api.route('/versions') @admin_required def list_versions(): """获取版本列表""" try: conn = get_db() with conn.cursor() as cur: cur.execute( "SELECT id, version, file_url, created_at FROM web_config ORDER BY created_at DESC" ) rows = cur.fetchall() conn.close() items = [ { 'id': r['id'], 'version': r['version'] or '', 'file_url': r['file_url'] or '', 'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '', } for r in rows ] return jsonify({'success': True, 'items': items}) except Exception as e: return jsonify({'success': False, 'error': str(e)}) @admin_api.route('/version', methods=['POST']) @admin_required def upload_version(): """接收版本号 + zip 文件,上传到 OSS,写入 web_config""" version = (request.form.get('version') or '').strip() if not version: return jsonify({'success': False, 'error': '请填写版本号'}) file_storage = request.files.get('file') if not file_storage or file_storage.filename == '': return jsonify({'success': False, 'error': '请选择要上传的 zip 压缩包'}) if not (file_storage.filename or '').lower().endswith('.zip'): return jsonify({'success': False, 'error': '仅支持 .zip 格式'}) try: file_content = file_storage.read() if not file_content: return jsonify({'success': False, 'error': '文件为空'}) safe_key = _safe_version_key(version) key = f"{bucket_path}versions/{safe_key}.zip" file_url = oss_upload_file(file_content, key) conn = get_db() with conn.cursor() as cur: cur.execute( "INSERT INTO web_config (version, file_url) VALUES (%s, %s)", (version, file_url) ) conn.commit() conn.close() return jsonify({ 'success': True, 'version': version, 'file_url': file_url, 'msg': '上传成功', }) except Exception as e: return jsonify({'success': False, 'error': str(e)}) # ---------- 店铺密钥管理 ---------- @admin_api.route('/shop-keys') @admin_required def list_shop_keys(): page = max(1, int(request.args.get('page', 1))) page_size = min(100, max(1, int(request.args.get('page_size', 15)))) result, error_response, status = _proxy_backend_java( 'GET', '/api/admin/shop-keys', params={'page': page, 'pageSize': page_size}, ) if error_response is not None: return error_response, status payload = result.get('data') or {} items = [ { 'id': item.get('id'), 'ziniao_account_name': item.get('ziniaoAccountName') or '', 'ziniao_token': item.get('ziniaoToken') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], } for item in (payload.get('items') or []) ] return jsonify({ 'success': True, 'items': items, 'total': payload.get('total') or 0, 'page': payload.get('page') or page, 'page_size': payload.get('pageSize') or page_size, }) @admin_api.route('/shop-key', methods=['POST']) @admin_required def create_shop_key(): data = request.get_json() or {} payload = { 'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(), 'ziniaoToken': (data.get('ziniao_token') or '').strip(), } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/shop-keys', json_data=payload, ) if error_response is not None: return error_response, status item = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '创建成功', 'item': { 'id': item.get('id'), 'ziniao_account_name': item.get('ziniaoAccountName') or '', 'ziniao_token': item.get('ziniaoToken') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/shop-key/', methods=['PUT']) @admin_required def update_shop_key(item_id): data = request.get_json() or {} payload = { 'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(), 'ziniaoToken': (data.get('ziniao_token') or '').strip(), } result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/shop-keys/{item_id}', json_data=payload, ) if error_response is not None: return error_response, status item = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '更新成功', 'item': { 'id': item.get('id'), 'ziniao_account_name': item.get('ziniaoAccountName') or '', 'ziniao_token': item.get('ziniaoToken') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/shop-key/', methods=['DELETE']) @admin_required def delete_shop_key(item_id): result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/shop-keys/{item_id}', ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'msg': result.get('message') or '删除成功', }) # ---------- 数据去重总数据 ---------- @admin_api.route('/dedupe-total-data') @admin_required def list_dedupe_total_data(): _, _, denied = _ensure_dedupe_total_data_access() if denied: return denied page = max(1, int(request.args.get('page', 1))) page_size = min(100, max(1, int(request.args.get('page_size', 15)))) keyword = (request.args.get('keyword') or '').strip() data, error_response, status = _proxy_backend_java( 'GET', '/api/admin/dedupe-total-data', params={'page': page, 'pageSize': page_size, 'keyword': keyword}, ) if error_response is not None: return error_response, status payload = data.get('data') or {} items = [ { 'id': item.get('id'), 'data_value': item.get('dataValue') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], } for item in (payload.get('items') or []) ] return jsonify({ 'success': True, 'items': items, 'total': payload.get('total') or 0, 'page': payload.get('page') or page, 'page_size': payload.get('pageSize') or page_size, }) @admin_api.route('/dedupe-total-data/import/') @admin_required def dedupe_total_data_import_progress(import_id): _, _, denied = _ensure_dedupe_total_data_access() if denied: return denied result, error_response, status = _proxy_backend_java( 'GET', f'/api/admin/dedupe-total-data/import/{import_id}', ) if error_response is not None: return error_response, status progress = result.get('data') or {} return jsonify({ 'success': True, 'progress': { 'status': progress.get('status') or 'pending', 'total_rows': progress.get('totalRows') or 0, 'processed_rows': progress.get('processedRows') or 0, 'asin_count': progress.get('asinCount') or 0, 'inserted_count': progress.get('insertedCount') or 0, 'skipped_count': progress.get('skippedCount') or 0, 'error_message': progress.get('errorMessage') or '', }, }) @admin_api.route('/dedupe-total-data/import', methods=['POST']) @admin_required def import_dedupe_total_data(): _, _, denied = _ensure_dedupe_total_data_access() if denied: return denied file_storage = request.files.get('file') if not file_storage or file_storage.filename == '': return jsonify({'success': False, 'error': '请选择 Excel 文件'}) files = { 'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream') } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/dedupe-total-data/import', files=files, ) if error_response is not None: return error_response, status summary = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '开始导入', 'import_id': summary.get('importId') or '', }) @admin_api.route('/dedupe-total-data/delete-import/') @admin_required def dedupe_total_data_delete_import_progress(import_id): _, _, denied = _ensure_dedupe_total_data_access() if denied: return denied result, error_response, status = _proxy_backend_java( 'GET', f'/api/admin/dedupe-total-data/delete-import/{import_id}', ) if error_response is not None: return error_response, status progress = result.get('data') or {} return jsonify({ 'success': True, 'progress': { 'status': progress.get('status') or 'pending', 'total_rows': progress.get('totalRows') or 0, 'processed_rows': progress.get('processedRows') or 0, 'asin_count': progress.get('asinCount') or 0, 'deleted_count': progress.get('insertedCount') or 0, 'skipped_count': progress.get('skippedCount') or 0, 'error_message': progress.get('errorMessage') or '', }, }) @admin_api.route('/dedupe-total-data/delete-import', methods=['POST']) @admin_required def delete_import_dedupe_total_data(): _, _, denied = _ensure_dedupe_total_data_access() if denied: return denied file_storage = request.files.get('file') if not file_storage or file_storage.filename == '': return jsonify({'success': False, 'error': '请选择 Excel 文件'}) files = { 'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream') } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/dedupe-total-data/delete-import', files=files, ) if error_response is not None: return error_response, status summary = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '开始删除', 'import_id': summary.get('importId') or '', }) @admin_api.route('/dedupe-total-data', methods=['POST']) @admin_required def create_dedupe_total_data(): _, _, denied = _ensure_dedupe_total_data_access() if denied: return denied data = request.get_json() or {} payload = {'dataValue': (data.get('data_value') or '').strip()} result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/dedupe-total-data', json_data=payload, ) if error_response is not None: return error_response, status item = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '创建成功', 'item': { 'id': item.get('id'), 'data_value': item.get('dataValue') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/dedupe-total-data/', methods=['PUT']) @admin_required def update_dedupe_total_data(item_id): _, _, denied = _ensure_dedupe_total_data_access() if denied: return denied data = request.get_json() or {} payload = {'dataValue': (data.get('data_value') or '').strip()} result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/dedupe-total-data/{item_id}', json_data=payload, ) if error_response is not None: return error_response, status item = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '更新成功', 'item': { 'id': item.get('id'), 'data_value': item.get('dataValue') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/dedupe-total-data/', methods=['DELETE']) @admin_required def delete_dedupe_total_data(item_id): _, _, denied = _ensure_dedupe_total_data_access() if denied: return denied result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/dedupe-total-data/{item_id}', ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'msg': result.get('message') or '删除成功', }) # ---------- 店铺管理 ---------- @admin_api.route('/shop-manages') @admin_required def list_shop_manages(): page = max(1, int(request.args.get('page', 1))) page_size = min(100, max(1, int(request.args.get('page_size', 15)))) result, error_response, status = _proxy_backend_java( 'GET', '/api/admin/shop-manages', params={'page': page, 'pageSize': page_size}, ) if error_response is not None: return error_response, status payload = result.get('data') or {} items = [ { 'id': item.get('id'), 'group_id': item.get('groupId'), 'group_name': item.get('groupName') or '', 'shop_name': item.get('shopName') or '', 'mall_name': item.get('mallName') or '', 'account': item.get('account') or '', 'password': item.get('passwordMasked') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], } for item in (payload.get('items') or []) ] return jsonify({ 'success': True, 'items': items, 'total': payload.get('total') or 0, 'page': payload.get('page') or page, 'page_size': payload.get('pageSize') or page_size, }) @admin_api.route('/shop-manage', methods=['POST']) @admin_required def create_shop_manage(): data = request.get_json() or {} payload = { 'groupId': data.get('group_id'), 'shopName': (data.get('shop_name') or '').strip(), 'mallName': (data.get('mall_name') or '').strip(), 'account': (data.get('account') or '').strip(), 'password': (data.get('password') or '').strip(), } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/shop-manages', json_data=payload, ) if error_response is not None: return error_response, status item = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '创建成功', 'item': { 'id': item.get('id'), 'group_id': item.get('groupId'), 'group_name': item.get('groupName') or '', 'shop_name': item.get('shopName') or '', 'mall_name': item.get('mallName') or '', 'account': item.get('account') or '', 'password': item.get('password') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/shop-manage/', methods=['PUT']) @admin_required def update_shop_manage(item_id): data = request.get_json() or {} payload = { 'groupId': data.get('group_id'), 'shopName': (data.get('shop_name') or '').strip(), 'mallName': (data.get('mall_name') or '').strip(), 'account': (data.get('account') or '').strip(), 'password': (data.get('password') or '').strip(), } result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/shop-manages/{item_id}', json_data=payload, ) if error_response is not None: return error_response, status item = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '更新成功', 'item': { 'id': item.get('id'), 'group_name': item.get('groupName') or '', 'shop_name': item.get('shopName') or '', 'mall_name': item.get('mallName') or '', 'account': item.get('account') or '', 'password': item.get('password') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/shop-manage/', methods=['DELETE']) @admin_required def delete_shop_manage(item_id): result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/shop-manages/{item_id}', ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'msg': result.get('message') or '删除成功', }) @admin_api.route('/shop-manage-groups') @admin_required def list_shop_manage_groups(): result, error_response, status = _proxy_backend_java( 'GET', '/api/admin/shop-manages/groups', ) if error_response is not None: return error_response, status items = [ { 'id': item.get('id'), 'group_name': item.get('groupName') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], } for item in (result.get('data') or []) ] return jsonify({'success': True, 'items': items}) @admin_api.route('/shop-manage-group', methods=['POST']) @admin_required def create_shop_manage_group(): data = request.get_json() or {} payload = {'groupName': (data.get('group_name') or '').strip()} result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/shop-manages/groups', json_data=payload, ) if error_response is not None: return error_response, status item = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '创建成功', 'item': { 'id': item.get('id'), 'group_name': item.get('groupName') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/shop-manage-group/', methods=['PUT']) @admin_required def update_shop_manage_group(item_id): data = request.get_json() or {} payload = {'groupName': (data.get('group_name') or '').strip()} result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/shop-manages/groups/{item_id}', json_data=payload, ) if error_response is not None: return error_response, status item = result.get('data') or {} return jsonify({ 'success': True, 'msg': result.get('message') or '更新成功', 'item': { 'id': item.get('id'), 'group_name': item.get('groupName') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/shop-manage-group/', methods=['DELETE']) @admin_required def delete_shop_manage_group(item_id): result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/shop-manages/groups/{item_id}', ) if error_response is not None: return error_response, status return jsonify({'success': True, 'msg': result.get('message') or '删除成功'})