commit 0368e85abe8664c62053a586811e32e77ca10dc6 Author: hera Date: Mon Apr 6 13:46:32 2026 +0000 Initial commit: Essential Oil Formula Cost Calculator diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8fddbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +*.pyc +__pycache__/ +deploy/kubeconfig +all_recipes_extracted.json +backups/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6a795ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY backend/ ./backend/ +COPY frontend/ ./frontend/ + +ENV DB_PATH=/data/oil_calculator.db +ENV FRONTEND_DIR=/app/frontend + +EXPOSE 8000 + +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..fb6d174 --- /dev/null +++ b/backend/database.py @@ -0,0 +1,306 @@ +import sqlite3 +import json +import os +import secrets + +DB_PATH = os.environ.get("DB_PATH", "/data/oil_calculator.db") + + +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA foreign_keys=ON") + return conn + + +def init_db(): + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + conn = get_db() + c = conn.cursor() + c.executescript(""" + CREATE TABLE IF NOT EXISTS oils ( + name TEXT PRIMARY KEY, + bottle_price REAL NOT NULL, + drop_count INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS recipes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + note TEXT DEFAULT '', + sort_order INTEGER DEFAULT 0 + ); + CREATE TABLE IF NOT EXISTS recipe_ingredients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, + oil_name TEXT NOT NULL, + drops REAL NOT NULL + ); + CREATE TABLE IF NOT EXISTS tags ( + name TEXT PRIMARY KEY + ); + CREATE TABLE IF NOT EXISTS recipe_tags ( + recipe_id INTEGER NOT NULL REFERENCES recipes(id) ON DELETE CASCADE, + tag_name TEXT NOT NULL, + PRIMARY KEY (recipe_id, tag_name) + ); + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + token TEXT UNIQUE NOT NULL, + role TEXT NOT NULL DEFAULT 'viewer', + display_name TEXT, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + action TEXT NOT NULL, + target_type TEXT, + target_id TEXT, + target_name TEXT, + detail TEXT, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS user_inventory ( + user_id INTEGER NOT NULL, + oil_name TEXT NOT NULL, + PRIMARY KEY (user_id, oil_name) + ); + CREATE TABLE IF NOT EXISTS profit_projects ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + ingredients TEXT NOT NULL DEFAULT '[]', + pricing REAL DEFAULT 0, + note TEXT DEFAULT '', + created_by INTEGER, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS user_diary ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + source_recipe_id INTEGER, + name TEXT NOT NULL, + ingredients TEXT NOT NULL DEFAULT '[]', + note TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS diary_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + diary_id INTEGER NOT NULL REFERENCES user_diary(id) ON DELETE CASCADE, + content TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS user_favorites ( + user_id INTEGER NOT NULL, + recipe_id INTEGER NOT NULL, + created_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (user_id, recipe_id) + ); + CREATE TABLE IF NOT EXISTS search_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + query TEXT NOT NULL, + matched_count INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS notifications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + target_role TEXT NOT NULL DEFAULT 'admin', + title TEXT NOT NULL, + body TEXT, + is_read INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS category_modules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + subtitle TEXT DEFAULT '', + icon TEXT DEFAULT '🌿', + bg_image TEXT DEFAULT '', + color_from TEXT DEFAULT '#7a9e7e', + color_to TEXT DEFAULT '#5a7d5e', + tag_name TEXT NOT NULL, + sort_order INTEGER DEFAULT 0 + ); + """) + + # Migration: add password and brand fields to users if missing + user_cols = [row[1] for row in c.execute("PRAGMA table_info(users)").fetchall()] + if "password" not in user_cols: + c.execute("ALTER TABLE users ADD COLUMN password TEXT") + + # Create bug_reports table + c.execute("""CREATE TABLE IF NOT EXISTS bug_reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + content TEXT NOT NULL, + is_resolved INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')) + )""") + + # Migration: add priority to bug_reports + bug_cols = [row[1] for row in c.execute("PRAGMA table_info(bug_reports)").fetchall()] + if bug_cols and "priority" not in bug_cols: + c.execute("ALTER TABLE bug_reports ADD COLUMN priority INTEGER DEFAULT 2") + + # Create bug_comments table for activity log + c.execute("""CREATE TABLE IF NOT EXISTS bug_comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + bug_id INTEGER NOT NULL REFERENCES bug_reports(id) ON DELETE CASCADE, + user_id INTEGER, + action TEXT NOT NULL, + content TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now')) + )""") + if "qr_code" not in user_cols: + c.execute("ALTER TABLE users ADD COLUMN qr_code TEXT") + if "brand_logo" not in user_cols: + c.execute("ALTER TABLE users ADD COLUMN brand_logo TEXT") + if "brand_name" not in user_cols: + c.execute("ALTER TABLE users ADD COLUMN brand_name TEXT") + if "brand_bg" not in user_cols: + c.execute("ALTER TABLE users ADD COLUMN brand_bg TEXT") + if "brand_align" not in user_cols: + c.execute("ALTER TABLE users ADD COLUMN brand_align TEXT DEFAULT 'center'") + + # Migration: add tags to user_diary + diary_cols = [row[1] for row in c.execute("PRAGMA table_info(user_diary)").fetchall()] + if diary_cols and "tags" not in diary_cols: + c.execute("ALTER TABLE user_diary ADD COLUMN tags TEXT DEFAULT '[]'") + + # Migration: business verification + if "business_verified" not in user_cols: + c.execute("ALTER TABLE users ADD COLUMN business_verified INTEGER DEFAULT 0") + + c.execute("""CREATE TABLE IF NOT EXISTS business_applications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + business_name TEXT, + document TEXT, + status TEXT DEFAULT 'pending', + reject_reason TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now')), + reviewed_at TEXT + )""") + + # Translation suggestions table + c.execute("""CREATE TABLE IF NOT EXISTS translation_suggestions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + recipe_id INTEGER, + recipe_name TEXT NOT NULL, + suggested_en TEXT NOT NULL, + user_id INTEGER, + status TEXT DEFAULT 'pending', + created_at TEXT DEFAULT (datetime('now')) + )""") + + # Migration: add reject_reason to business_applications if missing + biz_cols = [row[1] for row in c.execute("PRAGMA table_info(business_applications)").fetchall()] + if biz_cols and "reject_reason" not in biz_cols: + c.execute("ALTER TABLE business_applications ADD COLUMN reject_reason TEXT DEFAULT ''") + + # Migration: add target_user_id to notifications for user-specific notifications + notif_cols = [row[1] for row in c.execute("PRAGMA table_info(notifications)").fetchall()] + if "target_user_id" not in notif_cols: + c.execute("ALTER TABLE notifications ADD COLUMN target_user_id INTEGER") + + # Migration: add assigned_to to bug_reports + bug_cols2 = [row[1] for row in c.execute("PRAGMA table_info(bug_reports)").fetchall()] + if "assigned_to" not in bug_cols2: + c.execute("ALTER TABLE bug_reports ADD COLUMN assigned_to INTEGER") + + # Migration: add version to recipes for optimistic locking + recipe_cols = [row[1] for row in c.execute("PRAGMA table_info(recipes)").fetchall()] + if "version" not in recipe_cols: + c.execute("ALTER TABLE recipes ADD COLUMN version INTEGER DEFAULT 1") + + # Migration: add retail_price and is_active to oils if missing + oil_cols = [row[1] for row in c.execute("PRAGMA table_info(oils)").fetchall()] + if "retail_price" not in oil_cols: + c.execute("ALTER TABLE oils ADD COLUMN retail_price REAL") + if "is_active" not in oil_cols: + c.execute("ALTER TABLE oils ADD COLUMN is_active INTEGER DEFAULT 1") + + # Migration: add new columns to category_modules if missing + cat_cols = [row[1] for row in c.execute("PRAGMA table_info(category_modules)").fetchall()] + if cat_cols and "bg_image" not in cat_cols: + c.execute("DROP TABLE category_modules") + c.execute("""CREATE TABLE category_modules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, subtitle TEXT DEFAULT '', icon TEXT DEFAULT '🌿', + bg_image TEXT DEFAULT '', color_from TEXT DEFAULT '#7a9e7e', color_to TEXT DEFAULT '#5a7d5e', + tag_name TEXT NOT NULL, sort_order INTEGER DEFAULT 0)""") + + # Migration: add owner_id to recipes if missing + cols = [row[1] for row in c.execute("PRAGMA table_info(recipes)").fetchall()] + if "owner_id" not in cols: + c.execute("ALTER TABLE recipes ADD COLUMN owner_id INTEGER") + if "updated_by" not in cols: + c.execute("ALTER TABLE recipes ADD COLUMN updated_by INTEGER") + + # Seed admin user if no users exist + count = c.execute("SELECT COUNT(*) FROM users").fetchone()[0] + if count == 0: + admin_token = os.environ.get("ADMIN_TOKEN", secrets.token_hex(24)) + c.execute( + "INSERT INTO users (username, token, role, display_name) VALUES (?, ?, ?, ?)", + ("hera", admin_token, "admin", "Hera"), + ) + admin_id = c.lastrowid + # Assign all existing recipes to admin + c.execute("UPDATE recipes SET owner_id = ? WHERE owner_id IS NULL", (admin_id,)) + print(f"[INIT] Admin user created. Token: {admin_token}") + + conn.commit() + conn.close() + + +def log_audit(conn, user_id, action, target_type=None, target_id=None, target_name=None, detail=None): + conn.execute( + "INSERT INTO audit_log (user_id, action, target_type, target_id, target_name, detail) " + "VALUES (?, ?, ?, ?, ?, ?)", + (user_id, action, target_type, str(target_id) if target_id else None, target_name, detail), + ) + + +def seed_defaults(default_oils_meta: dict, default_recipes: list): + """Seed DB with defaults if empty.""" + conn = get_db() + c = conn.cursor() + + # Seed oils + count = c.execute("SELECT COUNT(*) FROM oils").fetchone()[0] + if count == 0: + for name, meta in default_oils_meta.items(): + c.execute( + "INSERT OR IGNORE INTO oils (name, bottle_price, drop_count) VALUES (?, ?, ?)", + (name, meta["bottlePrice"], meta["dropCount"]), + ) + + # Seed recipes + count = c.execute("SELECT COUNT(*) FROM recipes").fetchone()[0] + if count == 0: + # Get admin user id for ownership + admin = c.execute("SELECT id FROM users WHERE role = 'admin' LIMIT 1").fetchone() + admin_id = admin[0] if admin else None + for r in default_recipes: + c.execute( + "INSERT INTO recipes (name, note, owner_id) VALUES (?, ?, ?)", + (r["name"], r.get("note", ""), admin_id), + ) + rid = c.lastrowid + for ing in r["ingredients"]: + c.execute( + "INSERT INTO recipe_ingredients (recipe_id, oil_name, drops) VALUES (?, ?, ?)", + (rid, ing["oil"], ing["drops"]), + ) + for tag in r.get("tags", []): + c.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,)) + c.execute( + "INSERT OR IGNORE INTO recipe_tags (recipe_id, tag_name) VALUES (?, ?)", + (rid, tag), + ) + + conn.commit() + conn.close() diff --git a/backend/defaults.json b/backend/defaults.json new file mode 100644 index 0000000..a8227f6 --- /dev/null +++ b/backend/defaults.json @@ -0,0 +1,8091 @@ +{ + "oils_meta": { + "小豆蔻": { + "bottlePrice": 250, + "dropCount": 93 + }, + "芹菜籽": { + "bottlePrice": 310, + "dropCount": 280 + }, + "芫荽": { + "bottlePrice": 220, + "dropCount": 280 + }, + "小茴香": { + "bottlePrice": 145, + "dropCount": 280 + }, + "生姜": { + "bottlePrice": 415, + "dropCount": 280 + }, + "姜黄": { + "bottlePrice": 275, + "dropCount": 280 + }, + "缬草": { + "bottlePrice": 485, + "dropCount": 280 + }, + "岩兰草": { + "bottlePrice": 520, + "dropCount": 280 + }, + "侧柏": { + "bottlePrice": 195, + "dropCount": 93 + }, + "桦木": { + "bottlePrice": 475, + "dropCount": 93 + }, + "雪松": { + "bottlePrice": 130, + "dropCount": 280 + }, + "斯里兰卡肉桂皮": { + "bottlePrice": 275, + "dropCount": 93 + }, + "夏威夷檀香": { + "bottlePrice": 615, + "dropCount": 93 + }, + "檀香": { + "bottlePrice": 715, + "dropCount": 93 + }, + "古巴香脂": { + "bottlePrice": 279, + "dropCount": 280 + }, + "乳香": { + "bottlePrice": 630, + "dropCount": 280 + }, + "枫香": { + "bottlePrice": 240, + "dropCount": 280 + }, + "没药": { + "bottlePrice": 585, + "dropCount": 280 + }, + "罗勒": { + "bottlePrice": 270, + "dropCount": 280 + }, + "黑云杉": { + "bottlePrice": 170, + "dropCount": 93 + }, + "芫荽叶": { + "bottlePrice": 230, + "dropCount": 280 + }, + "香茅": { + "bottlePrice": 170, + "dropCount": 280 + }, + "丝柏": { + "bottlePrice": 155, + "dropCount": 280 + }, + "道格拉斯冷杉": { + "bottlePrice": 195, + "dropCount": 93 + }, + "尤加利": { + "bottlePrice": 185, + "dropCount": 280 + }, + "扁柏": { + "bottlePrice": 230, + "dropCount": 93 + }, + "柠檬尤加利": { + "bottlePrice": 120, + "dropCount": 280 + }, + "柠檬草": { + "bottlePrice": 115, + "dropCount": 280 + }, + "马郁兰": { + "bottlePrice": 195, + "dropCount": 280 + }, + "香蜂草": { + "bottlePrice": 810, + "dropCount": 93 + }, + "麦卢卡": { + "bottlePrice": 430, + "dropCount": 93 + }, + "西班牙牛至": { + "bottlePrice": 225, + "dropCount": 280 + }, + "广藿香": { + "bottlePrice": 270, + "dropCount": 280 + }, + "椒样薄荷": { + "bottlePrice": 210, + "dropCount": 280 + }, + "苦橙叶": { + "bottlePrice": 220, + "dropCount": 280 + }, + "迷迭香": { + "bottlePrice": 175, + "dropCount": 280 + }, + "西伯利亚冷杉": { + "bottlePrice": 170, + "dropCount": 280 + }, + "西班牙鼠尾草": { + "bottlePrice": 230, + "dropCount": 280 + }, + "绿薄荷": { + "bottlePrice": 250, + "dropCount": 280 + }, + "茶树": { + "bottlePrice": 195, + "dropCount": 280 + }, + "百里香": { + "bottlePrice": 280, + "dropCount": 280 + }, + "冬青": { + "bottlePrice": 235, + "dropCount": 280 + }, + "蓝艾菊": { + "bottlePrice": 700, + "dropCount": 93 + }, + "快乐鼠尾草": { + "bottlePrice": 325, + "dropCount": 280 + }, + "丁香花蕾": { + "bottlePrice": 170, + "dropCount": 280 + }, + "天竺葵": { + "bottlePrice": 385, + "dropCount": 280 + }, + "永久花": { + "bottlePrice": 665, + "dropCount": 93 + }, + "茉莉": { + "bottlePrice": 1210, + "dropCount": 46 + }, + "薰衣草": { + "bottlePrice": 230, + "dropCount": 280 + }, + "罗马洋甘菊": { + "bottlePrice": 420, + "dropCount": 93 + }, + "依兰依兰": { + "bottlePrice": 350, + "dropCount": 280 + }, + "佛手柑": { + "bottlePrice": 345, + "dropCount": 280 + }, + "黑胡椒": { + "bottlePrice": 190, + "dropCount": 93 + }, + "圆柚": { + "bottlePrice": 165, + "dropCount": 280 + }, + "杜松浆果": { + "bottlePrice": 190, + "dropCount": 93 + }, + "柠檬": { + "bottlePrice": 120, + "dropCount": 280 + }, + "莱姆": { + "bottlePrice": 135, + "dropCount": 280 + }, + "山鸡椒": { + "bottlePrice": 190, + "dropCount": 280 + }, + "加州胡椒": { + "bottlePrice": 190, + "dropCount": 93 + }, + "红橘": { + "bottlePrice": 130, + "dropCount": 280 + }, + "野橘": { + "bottlePrice": 105, + "dropCount": 280 + }, + "乐释": { + "bottlePrice": 350, + "dropCount": 280 + }, + "赋活呼吸": { + "bottlePrice": 265, + "dropCount": 280 + }, + "芳香调理": { + "bottlePrice": 275, + "dropCount": 280 + }, + "安定情绪": { + "bottlePrice": 205, + "dropCount": 280 + }, + "柑橘清新": { + "bottlePrice": 190, + "dropCount": 280 + }, + "柑橘绚烂": { + "bottlePrice": 230, + "dropCount": 280 + }, + "温柔呵护": { + "bottlePrice": 445, + "dropCount": 280 + }, + "完美修护": { + "bottlePrice": 320, + "dropCount": 280 + }, + "舒缓": { + "bottlePrice": 305, + "dropCount": 93 + }, + "乐活": { + "bottlePrice": 305, + "dropCount": 280 + }, + "顺畅呼吸": { + "bottlePrice": 225, + "dropCount": 280 + }, + "愈创木": { + "bottlePrice": 160, + "dropCount": 280 + }, + "恬家": { + "bottlePrice": 220, + "dropCount": 280 + }, + "安宁神气": { + "bottlePrice": 335, + "dropCount": 280 + }, + "椰风香草": { + "bottlePrice": 310, + "dropCount": 93 + }, + "清醇薄荷": { + "bottlePrice": 255, + "dropCount": 280 + }, + "新瑞活力": { + "bottlePrice": 240, + "dropCount": 280 + }, + "保卫": { + "bottlePrice": 315, + "dropCount": 280 + }, + "净化清新": { + "bottlePrice": 205, + "dropCount": 280 + }, + "花样年华焕肤油": { + "bottlePrice": 680, + "dropCount": 186 + }, + "天然防护": { + "bottlePrice": 110, + "dropCount": 280 + }, + "西洋蓍草": { + "bottlePrice": 450, + "dropCount": 280 + }, + "元气": { + "bottlePrice": 230, + "dropCount": 280 + }, + "欢欣": { + "bottlePrice": 215, + "dropCount": 93 + }, + "抚慰": { + "bottlePrice": 335, + "dropCount": 93 + }, + "宽容": { + "bottlePrice": 195, + "dropCount": 93 + }, + "鼓舞": { + "bottlePrice": 205, + "dropCount": 93 + }, + "热情": { + "bottlePrice": 355, + "dropCount": 93 + }, + "静谧": { + "bottlePrice": 280, + "dropCount": 93 + }, + "椰子油": { + "bottlePrice": 115, + "dropCount": 2146 + }, + "植物空胶囊": { + "bottlePrice": 32.73, + "dropCount": 280 + }, + "玫瑰": { + "bottlePrice": 2680, + "dropCount": 93 + }, + "玫瑰呵护": { + "bottlePrice": 470, + "dropCount": 186 + }, + "茉莉呵护": { + "bottlePrice": 510, + "dropCount": 186 + }, + "橙花呵护": { + "bottlePrice": 430, + "dropCount": 186 + }, + "桂花呵护": { + "bottlePrice": 480, + "dropCount": 186 + }, + "木兰呵护": { + "bottlePrice": 380, + "dropCount": 186 + }, + "茶树呵护": { + "bottlePrice": 210, + "dropCount": 186 + }, + "特瑞活力": { + "bottlePrice": 350, + "dropCount": 280 + }, + "全神贯注": { + "bottlePrice": 320, + "dropCount": 186 + }, + "新清肌呵护": { + "bottlePrice": 260, + "dropCount": 186 + }, + "新清肌调理": { + "bottlePrice": 260, + "dropCount": 280 + }, + "当归": { + "bottlePrice": 450, + "dropCount": 93 + }, + "月桂叶": { + "bottlePrice": 280, + "dropCount": 280 + }, + "橙花": { + "bottlePrice": 1150, + "dropCount": 93 + }, + "桂花": { + "bottlePrice": 980, + "dropCount": 93 + }, + "穗甘松": { + "bottlePrice": 450, + "dropCount": 93 + }, + "玫瑰草": { + "bottlePrice": 180, + "dropCount": 280 + }, + "罗文莎叶": { + "bottlePrice": 250, + "dropCount": 280 + }, + "甜茴香": { + "bottlePrice": 180, + "dropCount": 280 + }, + "五味子": { + "bottlePrice": 280, + "dropCount": 280 + }, + "印蒿": { + "bottlePrice": 320, + "dropCount": 280 + }, + "柠檬香桃木": { + "bottlePrice": 250, + "dropCount": 280 + } + }, + "recipes": [ + { + "name": "酸痛包", + "note": "", + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 1 + }, + { + "oil": "舒缓", + "drops": 2 + }, + { + "oil": "芳香调理", + "drops": 1 + }, + { + "oil": "冬青", + "drops": 1 + }, + { + "oil": "柠檬草", + "drops": 1 + }, + { + "oil": "生姜", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "小v脸", + "note": "", + "ingredients": [ + { + "oil": "丝柏", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "西洋蓍草", + "drops": 1 + }, + { + "oil": "永久花", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "健脾化湿精油浴", + "note": "", + "ingredients": [ + { + "oil": "西伯利亚冷杉", + "drops": 3 + }, + { + "oil": "芫荽", + "drops": 3 + }, + { + "oil": "红橘", + "drops": 2 + }, + { + "oil": "椰子油", + "drops": 20 + } + ], + "tags": [] + }, + { + "name": "一夜好眠精油浴", + "note": "", + "ingredients": [ + { + "oil": "安宁神气", + "drops": 1 + }, + { + "oil": "岩兰草", + "drops": 1 + }, + { + "oil": "乐释", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 1 + }, + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "安定情绪", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "生发", + "note": "", + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 1 + }, + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "迷迭香", + "drops": 1 + }, + { + "oil": "丝柏", + "drops": 2 + }, + { + "oil": "生姜", + "drops": 1 + }, + { + "oil": "雪松", + "drops": 2 + }, + { + "oil": "薰衣草", + "drops": 1 + }, + { + "oil": "乳香", + "drops": 2 + }, + { + "oil": "安定情绪", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 15 + } + ], + "tags": [] + }, + { + "name": "湿疹舒缓", + "note": "", + "ingredients": [ + { + "oil": "广藿香", + "drops": 1 + }, + { + "oil": "绿薄荷", + "drops": 1 + }, + { + "oil": "麦卢卡", + "drops": 1 + }, + { + "oil": "永久花", + "drops": 1 + }, + { + "oil": "蓝艾菊", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "乳腺疏通", + "note": "", + "ingredients": [ + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 1 + }, + { + "oil": "丁香花蕾", + "drops": 1 + }, + { + "oil": "柑橘清新", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "缓解酸痛精油刮痧", + "note": "", + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 1 + }, + { + "oil": "舒缓", + "drops": 2 + }, + { + "oil": "芳香调理", + "drops": 1 + }, + { + "oil": "冬青", + "drops": 1 + }, + { + "oil": "柠檬草", + "drops": 1 + }, + { + "oil": "生姜", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "灰指甲", + "note": "", + "ingredients": [ + { + "oil": "西班牙牛至", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 6 + } + ], + "tags": [] + }, + { + "name": "白发转黑", + "note": "", + "ingredients": [ + { + "oil": "乳香", + "drops": 2 + }, + { + "oil": "快乐鼠尾草", + "drops": 1 + }, + { + "oil": "依兰依兰", + "drops": 1 + }, + { + "oil": "生姜", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 1 + }, + { + "oil": "扁柏", + "drops": 1 + }, + { + "oil": "雪松", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "紫外线修复", + "note": "有艾草时可加入艾草", + "ingredients": [ + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "西洋蓍草", + "drops": 1 + }, + { + "oil": "蓝艾菊", + "drops": 1 + }, + { + "oil": "麦卢卡", + "drops": 1 + }, + { + "oil": "侧柏", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 15 + } + ], + "tags": [] + }, + { + "name": "瘦身带脉", + "note": "", + "ingredients": [ + { + "oil": "丝柏", + "drops": 1 + }, + { + "oil": "圆柚", + "drops": 1 + }, + { + "oil": "新瑞活力", + "drops": 1 + }, + { + "oil": "永久花", + "drops": 1 + }, + { + "oil": "黑胡椒", + "drops": 1 + }, + { + "oil": "姜黄", + "drops": 1 + }, + { + "oil": "柠檬", + "drops": 1 + }, + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 15 + } + ], + "tags": [] + }, + { + "name": "私密护理", + "note": "", + "ingredients": [ + { + "oil": "西洋蓍草", + "drops": 2 + }, + { + "oil": "没药", + "drops": 2 + }, + { + "oil": "快乐鼠尾草", + "drops": 1 + }, + { + "oil": "依兰依兰", + "drops": 1 + }, + { + "oil": "茶树", + "drops": 0.5 + }, + { + "oil": "丝柏", + "drops": 0.5 + }, + { + "oil": "椰子油", + "drops": 8 + }, + { + "oil": "植物空胶囊", + "drops": 1 + } + ], + "tags": [] + }, + { + "name": "脚气/头皮屑", + "note": "", + "ingredients": [ + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 6 + } + ], + "tags": [] + }, + { + "name": "痘痘", + "note": "", + "ingredients": [ + { + "oil": "广藿香", + "drops": 1 + }, + { + "oil": "姜黄", + "drops": 1 + }, + { + "oil": "西洋蓍草", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "麦卢卡", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 20 + } + ], + "tags": [] + }, + { + "name": "淋巴排毒", + "note": "", + "ingredients": [ + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "丝柏", + "drops": 1 + }, + { + "oil": "圆柚", + "drops": 1 + }, + { + "oil": "迷迭香", + "drops": 1 + }, + { + "oil": "杜松浆果", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "驱寒/祛湿精油浴", + "note": "有艾草时可加入", + "ingredients": [ + { + "oil": "杜松浆果", + "drops": 3 + }, + { + "oil": "广藿香", + "drops": 3 + }, + { + "oil": "生姜", + "drops": 2 + }, + { + "oil": "椰子油", + "drops": 20 + } + ], + "tags": [] + }, + { + "name": "荷尔蒙调节/更年期护理精油浴", + "note": "", + "ingredients": [ + { + "oil": "依兰依兰", + "drops": 2 + }, + { + "oil": "温柔呵护", + "drops": 2 + }, + { + "oil": "天竺葵", + "drops": 2 + }, + { + "oil": "快乐鼠尾草", + "drops": 2 + }, + { + "oil": "岩兰草", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "情绪管理", + "note": "", + "ingredients": [ + { + "oil": "抚慰", + "drops": 1 + }, + { + "oil": "热情", + "drops": 1 + }, + { + "oil": "欢欣", + "drops": 1 + }, + { + "oil": "鼓舞", + "drops": 1 + }, + { + "oil": "宽容", + "drops": 1 + }, + { + "oil": "静谧", + "drops": 1 + }, + { + "oil": "椰子油", + "drops": 10 + } + ], + "tags": [] + }, + { + "name": "发膜", + "note": "", + "ingredients": [ + { + "oil": "乳香", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 2 + }, + { + "oil": "生姜", + "drops": 2 + } + ], + "tags": [] + }, + { + "name": "脾胃养护1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 10 + }, + { + "oil": "红橘", + "drops": 20 + }, + { + "oil": "乐活", + "drops": 20 + }, + { + "oil": "广藿香", + "drops": 15 + }, + { + "oil": "岩兰草", + "drops": 10 + } + ] + }, + { + "name": "脾胃养护2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "红橘", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 10 + }, + { + "oil": "乐活", + "drops": 15 + } + ] + }, + { + "name": "招财开运油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "夏威夷檀香", + "drops": 1 + }, + { + "oil": "岩兰草", + "drops": 1 + }, + { + "oil": "天竺葵", + "drops": 2 + }, + { + "oil": "佛手柑", + "drops": 10 + }, + { + "oil": "野橘", + "drops": 20 + } + ] + }, + { + "name": "植物热玛吉(玫瑰纯油版)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西洋蓍草", + "drops": 20 + }, + { + "oil": "玫瑰", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "植物热玛吉(玫瑰呵护版)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西洋蓍草", + "drops": 20 + }, + { + "oil": "玫瑰", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "十全大补", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "快乐鼠尾草", + "drops": 15 + }, + { + "oil": "花样年华焕肤油", + "drops": 10 + }, + { + "oil": "温柔呵护", + "drops": 8 + }, + { + "oil": "玫瑰呵护", + "drops": 15 + }, + { + "oil": "茉莉呵护", + "drops": 15 + }, + { + "oil": "温柔呵护", + "drops": 10 + } + ] + }, + { + "name": "豪华头疗", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 25 + }, + { + "oil": "丝柏", + "drops": 15 + }, + { + "oil": "夏威夷檀香", + "drops": 10 + }, + { + "oil": "完美修护", + "drops": 20 + }, + { + "oil": "乳香", + "drops": 20 + }, + { + "oil": "生姜", + "drops": 15 + }, + { + "oil": "雪松", + "drops": 15 + }, + { + "oil": "马郁兰", + "drops": 10 + }, + { + "oil": "安定情绪", + "drops": 20 + }, + { + "oil": "依兰依兰", + "drops": 10 + } + ] + }, + { + "name": "酸痛包1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "舒缓", + "drops": 10 + }, + { + "oil": "芳香调理", + "drops": 5 + }, + { + "oil": "冬青", + "drops": 10 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "西班牙牛至", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "酸痛包2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "舒缓", + "drops": 5 + }, + { + "oil": "芳香调理", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "明目青睐1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "柠檬草", + "drops": 6 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 9 + }, + { + "oil": "花样年华焕肤油", + "drops": 10 + }, + { + "oil": "快乐鼠尾草", + "drops": 10 + } + ] + }, + { + "name": "明目青睐2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "芳香调理", + "drops": 3 + }, + { + "oil": "柠檬草", + "drops": 3 + }, + { + "oil": "快乐鼠尾草", + "drops": 7 + }, + { + "oil": "乳香", + "drops": 7 + } + ] + }, + { + "name": "带脉排毒瘦腰1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "广藿香", + "drops": 5 + }, + { + "oil": "黑胡椒", + "drops": 10 + }, + { + "oil": "天竺葵", + "drops": 20 + }, + { + "oil": "新瑞活力", + "drops": 30 + }, + { + "oil": "丝柏", + "drops": 30 + }, + { + "oil": "乳香", + "drops": 20 + }, + { + "oil": "杜松浆果", + "drops": 20 + } + ] + }, + { + "name": "带脉瘦腰2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "杜松浆果", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "圆柚", + "drops": 10 + }, + { + "oil": "丝柏", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 7 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "新瑞活力", + "drops": 10 + } + ] + }, + { + "name": "缓解头痛(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "罗勒", + "drops": 15 + }, + { + "oil": "马郁兰", + "drops": 15 + }, + { + "oil": "舒缓", + "drops": 50 + } + ] + }, + { + "name": "1清咽止咳(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "没药", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 15 + }, + { + "oil": "尤加利", + "drops": 15 + }, + { + "oil": "小豆蔻", + "drops": 10 + }, + { + "oil": "顺畅呼吸", + "drops": 20 + }, + { + "oil": "西伯利亚冷杉", + "drops": 10 + } + ] + }, + { + "name": "清咽止咳2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "保卫", + "drops": 5 + }, + { + "oil": "顺畅呼吸", + "drops": 10 + } + ] + }, + { + "name": "提升免疫(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西班牙牛至", + "drops": 5 + }, + { + "oil": "侧柏", + "drops": 10 + }, + { + "oil": "百里香", + "drops": 10 + }, + { + "oil": "柠檬草", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 20 + }, + { + "oil": "茶树", + "drops": 10 + }, + { + "oil": "保卫", + "drops": 10 + } + ] + }, + { + "name": "护肝排毒(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "柠檬", + "drops": 20 + }, + { + "oil": "元气", + "drops": 20 + }, + { + "oil": "当归", + "drops": 1 + }, + { + "oil": "芹菜籽", + "drops": 10 + }, + { + "oil": "天竺葵", + "drops": 9 + }, + { + "oil": "迷迭香", + "drops": 15 + } + ] + }, + { + "name": "强心护心(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "百里香", + "drops": 10 + }, + { + "oil": "香蜂草", + "drops": 15 + }, + { + "oil": "古巴香脂", + "drops": 20 + }, + { + "oil": "依兰依兰", + "drops": 15 + }, + { + "oil": "快乐鼠尾草", + "drops": 15 + } + ] + }, + { + "name": "通鼻消炎(强)1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "尤加利", + "drops": 10 + }, + { + "oil": "蓝艾菊", + "drops": 5 + }, + { + "oil": "迷迭香", + "drops": 10 + }, + { + "oil": "顺畅呼吸", + "drops": 20 + } + ] + }, + { + "name": "通鼻消炎2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "尤加利", + "drops": 10 + }, + { + "oil": "顺畅呼吸", + "drops": 15 + } + ] + }, + { + "name": "过敏湿疹(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗马洋甘菊", + "drops": 10 + }, + { + "oil": "蓝艾菊", + "drops": 20 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "香蜂草", + "drops": 15 + }, + { + "oil": "绿薄荷", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 10 + }, + { + "oil": "广藿香", + "drops": 15 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "强肾化水(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茉莉呵护", + "drops": 10 + }, + { + "oil": "檀香", + "drops": 15 + }, + { + "oil": "黑胡椒", + "drops": 10 + }, + { + "oil": "依兰依兰", + "drops": 10 + }, + { + "oil": "古巴香脂", + "drops": 10 + }, + { + "oil": "杜松浆果", + "drops": 20 + } + ] + }, + { + "name": "防疫病毒(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "尤加利", + "drops": 5 + }, + { + "oil": "侧柏", + "drops": 5 + }, + { + "oil": "百里香", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 10 + }, + { + "oil": "保卫", + "drops": 10 + } + ] + }, + { + "name": "消富贵包(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "芳香调理", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 10 + }, + { + "oil": "舒缓", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 10 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "淋巴排毒(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "柑橘清新", + "drops": 5 + }, + { + "oil": "丁香花蕾", + "drops": 10 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "没药", + "drops": 20 + }, + { + "oil": "乳香", + "drops": 20 + } + ] + }, + { + "name": "祛湿化滞(强)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "黑胡椒", + "drops": 8 + }, + { + "oil": "斯里兰卡肉桂皮", + "drops": 6 + }, + { + "oil": "圆柚", + "drops": 9 + }, + { + "oil": "岩兰草", + "drops": 8 + }, + { + "oil": "广藿香", + "drops": 9 + }, + { + "oil": "姜黄", + "drops": 10 + }, + { + "oil": "西洋蓍草", + "drops": 10 + } + ] + }, + { + "name": "肺部结节", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "柑橘清新", + "drops": 5 + }, + { + "oil": "赋活呼吸", + "drops": 5 + }, + { + "oil": "天竺葵", + "drops": 10 + } + ] + }, + { + "name": "尿路感染1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "天竺葵", + "drops": 10 + }, + { + "oil": "杜松浆果", + "drops": 5 + }, + { + "oil": "西班牙牛至", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "罗勒", + "drops": 5 + }, + { + "oil": "保卫", + "drops": 5 + }, + { + "oil": "柠檬", + "drops": 5 + } + ] + }, + { + "name": "尿路感染2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西班牙牛至", + "drops": 1 + }, + { + "oil": "柠檬草", + "drops": 1 + } + ] + }, + { + "name": "养前列腺", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "檀香", + "drops": 3 + }, + { + "oil": "杜松浆果", + "drops": 8 + }, + { + "oil": "斯里兰卡肉桂皮", + "drops": 2 + }, + { + "oil": "芳香调理", + "drops": 6 + }, + { + "oil": "茉莉", + "drops": 1 + }, + { + "oil": "快乐鼠尾草", + "drops": 3 + }, + { + "oil": "五味子", + "drops": 15 + }, + { + "oil": "百里香", + "drops": 2 + } + ] + }, + { + "name": "皮外损伤", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "茶树", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 8 + }, + { + "oil": "没药", + "drops": 4 + }, + { + "oil": "永久花", + "drops": 4 + } + ] + }, + { + "name": "消脂肪肝", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "杜松浆果", + "drops": 5 + }, + { + "oil": "元气", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 2 + }, + { + "oil": "柠檬", + "drops": 3 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "新瑞活力", + "drops": 10 + } + ] + }, + { + "name": "平衡血糖", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "新瑞活力", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "斯里兰卡肉桂皮", + "drops": 3 + }, + { + "oil": "迷迭香", + "drops": 3 + }, + { + "oil": "芫荽", + "drops": 3 + }, + { + "oil": "山鸡椒", + "drops": 3 + }, + { + "oil": "天竺葵", + "drops": 3 + } + ] + }, + { + "name": "肚痛腹泻", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乐活", + "drops": 16 + }, + { + "oil": "柠檬草", + "drops": 8 + }, + { + "oil": "生姜", + "drops": 6 + }, + { + "oil": "西班牙牛至", + "drops": 6 + }, + { + "oil": "罗勒", + "drops": 4 + } + ] + }, + { + "name": "韧带扭伤", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "芳香调理", + "drops": 8 + }, + { + "oil": "姜黄", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 8 + }, + { + "oil": "西班牙牛至", + "drops": 3 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "罗勒", + "drops": 2 + }, + { + "oil": "古巴香脂", + "drops": 6 + } + ] + }, + { + "name": "血脂稳定", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "新瑞活力", + "drops": 10 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "小茴香", + "drops": 5 + }, + { + "oil": "罗勒", + "drops": 5 + }, + { + "oil": "元气", + "drops": 5 + } + ] + }, + { + "name": "尿床尿频", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "杜松浆果", + "drops": 16 + }, + { + "oil": "柠檬草", + "drops": 12 + }, + { + "oil": "侧柏", + "drops": 8 + }, + { + "oil": "檀香", + "drops": 4 + } + ] + }, + { + "name": "冻疮修复", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "黑胡椒", + "drops": 3 + }, + { + "oil": "没药", + "drops": 3 + }, + { + "oil": "迷迭香", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "天竺葵", + "drops": 10 + } + ] + }, + { + "name": "皮下囊肿", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西洋蓍草", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "百里香", + "drops": 5 + }, + { + "oil": "西班牙牛至", + "drops": 5 + }, + { + "oil": "广藿香", + "drops": 10 + }, + { + "oil": "丁香花蕾", + "drops": 5 + } + ] + }, + { + "name": "疏肝解郁", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "芹菜籽", + "drops": 2 + }, + { + "oil": "岩兰草", + "drops": 5 + }, + { + "oil": "元气", + "drops": 8 + }, + { + "oil": "佛手柑", + "drops": 5 + }, + { + "oil": "罗马洋甘菊", + "drops": 5 + } + ] + }, + { + "name": "大脑抗衰", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "特瑞活力", + "drops": 10 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "迷迭香", + "drops": 5 + }, + { + "oil": "乐释", + "drops": 5 + }, + { + "oil": "广藿香", + "drops": 5 + }, + { + "oil": "古巴香脂", + "drops": 5 + } + ] + }, + { + "name": "神经麻木", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "特瑞活力", + "drops": 10 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "百里香", + "drops": 5 + }, + { + "oil": "古巴香脂", + "drops": 10 + }, + { + "oil": "马郁兰", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "口唇疱疹1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "罗马洋甘菊", + "drops": 7 + }, + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "玫瑰草", + "drops": 3 + }, + { + "oil": "没药", + "drops": 5 + } + ] + }, + { + "name": "口唇疱疹2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "薰衣草", + "drops": 6 + }, + { + "oil": "罗马洋甘菊", + "drops": 6 + }, + { + "oil": "罗文莎叶", + "drops": 6 + } + ] + }, + { + "name": "春季用油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "黑云杉", + "drops": 3 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "杜松浆果", + "drops": 5 + }, + { + "oil": "芹菜籽", + "drops": 5 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "莱姆", + "drops": 10 + }, + { + "oil": "元气", + "drops": 10 + } + ] + }, + { + "name": "夏季用油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "杜松浆果", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 5 + }, + { + "oil": "圆柚", + "drops": 5 + }, + { + "oil": "斯里兰卡肉桂皮", + "drops": 3 + }, + { + "oil": "西伯利亚冷杉", + "drops": 5 + }, + { + "oil": "广藿香", + "drops": 7 + }, + { + "oil": "乐活", + "drops": 10 + } + ] + }, + { + "name": "秋季用油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西伯利亚冷杉", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "小豆蔻", + "drops": 3 + }, + { + "oil": "丁香花蕾", + "drops": 2 + }, + { + "oil": "迷迭香", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 7 + }, + { + "oil": "顺畅呼吸", + "drops": 10 + } + ] + }, + { + "name": "冬季用油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茉莉呵护", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "雪松", + "drops": 5 + }, + { + "oil": "黑胡椒", + "drops": 2 + }, + { + "oil": "檀香", + "drops": 3 + }, + { + "oil": "五味子", + "drops": 3 + }, + { + "oil": "杜松浆果", + "drops": 5 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "祛湿丸子", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "斯里兰卡肉桂皮", + "drops": 1 + }, + { + "oil": "圆柚", + "drops": 2 + }, + { + "oil": "姜黄", + "drops": 2 + }, + { + "oil": "黑胡椒", + "drops": 1 + }, + { + "oil": "岩兰草", + "drops": 2 + }, + { + "oil": "广藿香", + "drops": 2 + }, + { + "oil": "西洋蓍草", + "drops": 2 + } + ] + }, + { + "name": "三伏排湿", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "杜松浆果", + "drops": 5 + }, + { + "oil": "芳香调理", + "drops": 5 + }, + { + "oil": "圆柚", + "drops": 5 + }, + { + "oil": "元气", + "drops": 5 + }, + { + "oil": "新瑞活力", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "广藿香", + "drops": 5 + } + ] + }, + { + "name": "三伏晒背", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "罗马洋甘菊", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "蓝艾菊", + "drops": 3 + }, + { + "oil": "西洋蓍草", + "drops": 10 + } + ] + }, + { + "name": "三伏百会", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "特瑞活力", + "drops": 10 + }, + { + "oil": "古巴香脂", + "drops": 10 + } + ] + }, + { + "name": "三伏八髎", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "印蒿", + "drops": 3 + }, + { + "oil": "温柔呵护", + "drops": 4 + }, + { + "oil": "完美修护", + "drops": 6 + }, + { + "oil": "花样年华焕肤油", + "drops": 4 + } + ] + }, + { + "name": "三伏大椎", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "斯里兰卡肉桂皮", + "drops": 5 + }, + { + "oil": "檀香", + "drops": 5 + }, + { + "oil": "西洋蓍草", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "广藿香", + "drops": 10 + } + ] + }, + { + "name": "三伏檀中", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "夏威夷檀香", + "drops": 2 + }, + { + "oil": "香蜂草", + "drops": 30 + } + ] + }, + { + "name": "太伏太溪", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "元气", + "drops": 15 + }, + { + "oil": "特瑞活力", + "drops": 15 + } + ] + }, + { + "name": "三伏扶阳", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 8 + }, + { + "oil": "广藿香", + "drops": 3 + }, + { + "oil": "依兰依兰", + "drops": 3 + }, + { + "oil": "生姜", + "drops": 8 + }, + { + "oil": "檀香", + "drops": 5 + }, + { + "oil": "斯里兰卡肉桂皮", + "drops": 4 + }, + { + "oil": "当归", + "drops": 2 + }, + { + "oil": "黑胡椒", + "drops": 8 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "杜松浆果", + "drops": 8 + } + ] + }, + { + "name": "三阴交油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 6 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "杜松浆果", + "drops": 10 + }, + { + "oil": "快乐鼠尾草", + "drops": 4 + } + ] + }, + { + "name": "八虚用油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "元气", + "drops": 5 + }, + { + "oil": "圆柚", + "drops": 5 + }, + { + "oil": "新瑞活力", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "芳香调理", + "drops": 5 + } + ] + }, + { + "name": "冬日小火炉", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "山鸡椒", + "drops": 6 + }, + { + "oil": "生姜", + "drops": 6 + }, + { + "oil": "黑胡椒", + "drops": 6 + }, + { + "oil": "茉莉", + "drops": 2 + }, + { + "oil": "柠檬草", + "drops": 6 + }, + { + "oil": "天竺葵", + "drops": 6 + }, + { + "oil": "温柔呵护", + "drops": 10 + }, + { + "oil": "小茴香", + "drops": 4 + } + ] + }, + { + "name": "女性内分泌", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "依兰依兰", + "drops": 5 + }, + { + "oil": "快乐鼠尾草", + "drops": 5 + }, + { + "oil": "温柔呵护", + "drops": 10 + }, + { + "oil": "温柔呵护", + "drops": 5 + } + ] + }, + { + "name": "提升免疫力", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "茶树", + "drops": 20 + }, + { + "oil": "保卫", + "drops": 20 + } + ] + }, + { + "name": "面部精华", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 5 + }, + { + "oil": "雪松", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "西洋蓍草", + "drops": 10 + } + ] + }, + { + "name": "日常头疗", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "野橘", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "雪松", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 7 + }, + { + "oil": "安定情绪", + "drops": 7 + } + ] + }, + { + "name": "肝肾保护", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "杜松浆果", + "drops": 5 + }, + { + "oil": "柠檬", + "drops": 10 + }, + { + "oil": "元气", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 5 + } + ] + }, + { + "name": "情绪能量油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "柑橘绚烂", + "drops": 10 + }, + { + "oil": "柑橘清新", + "drops": 10 + }, + { + "oil": "安宁神气", + "drops": 5 + }, + { + "oil": "安定情绪", + "drops": 5 + } + ] + }, + { + "name": "养心宁神", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "依兰依兰", + "drops": 5 + }, + { + "oil": "古巴香脂", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 15 + } + ] + }, + { + "name": "安稳睡眠", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "安定情绪", + "drops": 10 + } + ] + }, + { + "name": "过敏体质", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗马洋甘菊", + "drops": 20 + }, + { + "oil": "椒样薄荷", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "柠檬", + "drops": 10 + } + ] + }, + { + "name": "淋巴结节", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "完美修护", + "drops": 7 + }, + { + "oil": "丝柏", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 7 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "芳香调理", + "drops": 5 + } + ] + }, + { + "name": "甲状腺结节", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "芳香调理", + "drops": 5 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "丁香花蕾", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "皮肤过敏", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗马洋甘菊", + "drops": 8 + }, + { + "oil": "西洋蓍草", + "drops": 10 + }, + { + "oil": "雪松", + "drops": 5 + }, + { + "oil": "侧柏", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "止鼾安睡", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "安宁神气", + "drops": 10 + }, + { + "oil": "罗勒", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 5 + }, + { + "oil": "道格拉斯冷杉", + "drops": 5 + }, + { + "oil": "百里香", + "drops": 5 + }, + { + "oil": "顺畅呼吸", + "drops": 10 + } + ] + }, + { + "name": "哮喘缓解", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "顺畅呼吸", + "drops": 10 + }, + { + "oil": "蓝艾菊", + "drops": 5 + }, + { + "oil": "柠檬", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "赋活呼吸", + "drops": 10 + } + ] + }, + { + "name": "带状疱疹", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "广藿香", + "drops": 3 + }, + { + "oil": "佛手柑", + "drops": 5 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "丁香花蕾", + "drops": 5 + }, + { + "oil": "百里香", + "drops": 5 + }, + { + "oil": "香蜂草", + "drops": 3 + }, + { + "oil": "完美修护", + "drops": 7 + }, + { + "oil": "没药", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 7 + } + ] + }, + { + "name": "夏日社痱", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗马洋甘菊", + "drops": 8 + }, + { + "oil": "椒样薄荷", + "drops": 8 + }, + { + "oil": "薰衣草", + "drops": 6 + }, + { + "oil": "广藿香", + "drops": 8 + } + ] + }, + { + "name": "耳聋耳鸣", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "香蜂草", + "drops": 3 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "罗勒", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "减脂瘦身", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 8 + }, + { + "oil": "杜松浆果", + "drops": 10 + }, + { + "oil": "圆柚", + "drops": 10 + }, + { + "oil": "迷迭香", + "drops": 7 + }, + { + "oil": "丝柏", + "drops": 15 + }, + { + "oil": "新瑞活力", + "drops": 20 + } + ] + }, + { + "name": "痔疮用油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "没药", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "天竺葵", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 10 + }, + { + "oil": "丝柏", + "drops": 15 + }, + { + "oil": "茶树", + "drops": 20 + } + ] + }, + { + "name": "静脉曲张1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "甜茴香", + "drops": 7 + }, + { + "oil": "丝柏", + "drops": 15 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "柠檬草", + "drops": 8 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "芳香调理", + "drops": 5 + } + ] + }, + { + "name": "静脉曲张2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 3 + }, + { + "oil": "黑胡椒", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 8 + }, + { + "oil": "丝柏", + "drops": 10 + } + ] + }, + { + "name": "痛风缓解", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "柠檬草", + "drops": 8 + }, + { + "oil": "西伯利亚冷杉", + "drops": 5 + }, + { + "oil": "芳香调理", + "drops": 10 + }, + { + "oil": "舒缓", + "drops": 5 + }, + { + "oil": "西班牙牛至", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "香蜂草", + "drops": 3 + } + ] + }, + { + "name": "滑膜炎症", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "丝柏", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "罗勒", + "drops": 5 + }, + { + "oil": "冬青", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "道格拉斯冷杉", + "drops": 5 + } + ] + }, + { + "name": "腱鞘炎症", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗勒", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "圆柚", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 10 + } + ] + }, + { + "name": "腰椎滑脱", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "丝柏", + "drops": 5 + }, + { + "oil": "舒缓", + "drops": 5 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "檀香", + "drops": 5 + }, + { + "oil": "冬青", + "drops": 5 + }, + { + "oil": "马郁兰", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "暖宫调经1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "温柔呵护", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 10 + }, + { + "oil": "黑胡椒", + "drops": 10 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "温柔呵护", + "drops": 25 + }, + { + "oil": "快乐鼠尾草", + "drops": 15 + } + ] + }, + { + "name": "暖宫调经2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "温柔呵护", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 6 + }, + { + "oil": "黑胡椒", + "drops": 6 + }, + { + "oil": "天竺葵", + "drops": 3 + }, + { + "oil": "温柔呵护", + "drops": 17 + }, + { + "oil": "快乐鼠尾草", + "drops": 10 + } + ] + }, + { + "name": "经期止痛", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "黑胡椒", + "drops": 5 + }, + { + "oil": "舒缓", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "温柔呵护", + "drops": 10 + }, + { + "oil": "温柔呵护", + "drops": 20 + }, + { + "oil": "薰衣草", + "drops": 10 + } + ] + }, + { + "name": "乳腺增生", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "迷迭香", + "drops": 5 + }, + { + "oil": "百里香", + "drops": 5 + }, + { + "oil": "丁香花蕾", + "drops": 5 + }, + { + "oil": "柑橘清新", + "drops": 10 + } + ] + }, + { + "name": "丰胸挺拨", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "小茴香", + "drops": 20 + }, + { + "oil": "依兰依兰", + "drops": 10 + }, + { + "oil": "温柔呵护", + "drops": 30 + }, + { + "oil": "西洋蓍草", + "drops": 30 + }, + { + "oil": "快乐鼠尾草", + "drops": 10 + } + ] + }, + { + "name": "私密紧致", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "玫瑰呵护", + "drops": 10 + }, + { + "oil": "茉莉呵护", + "drops": 10 + }, + { + "oil": "丝柏", + "drops": 15 + }, + { + "oil": "依兰依兰", + "drops": 10 + }, + { + "oil": "快乐鼠尾草", + "drops": 15 + } + ] + }, + { + "name": "乳腺结节1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "柑橘清新", + "drops": 5 + }, + { + "oil": "没药", + "drops": 10 + }, + { + "oil": "姜黄", + "drops": 10 + }, + { + "oil": "丁香花蕾", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 20 + } + ] + }, + { + "name": "乳腺结节2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "丁香花蕾", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "柑橘清新", + "drops": 10 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "柠檬草", + "drops": 5 + } + ] + }, + { + "name": "手脚冰凉", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 20 + }, + { + "oil": "天竺葵", + "drops": 10 + }, + { + "oil": "岩兰草", + "drops": 10 + }, + { + "oil": "柠檬草", + "drops": 10 + }, + { + "oil": "黑胡椒", + "drops": 15 + }, + { + "oil": "野橘", + "drops": 10 + } + ] + }, + { + "name": "修复腹直肌", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "丝柏", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "圆柚", + "drops": 10 + }, + { + "oil": "广藿香", + "drops": 10 + }, + { + "oil": "柠檬草", + "drops": 10 + } + ] + }, + { + "name": "子宫肌瘤1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "安定情绪", + "drops": 5 + }, + { + "oil": "西班牙牛至", + "drops": 3 + }, + { + "oil": "姜黄", + "drops": 10 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "子宫肌瘤2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "罗勒", + "drops": 6 + }, + { + "oil": "天竺葵", + "drops": 10 + } + ] + }, + { + "name": "卵巢囊肿1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "薰衣草", + "drops": 8 + }, + { + "oil": "完美修护", + "drops": 5 + }, + { + "oil": "温柔呵护", + "drops": 50 + }, + { + "oil": "斯里兰卡肉桂皮", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 8 + }, + { + "oil": "迷迭香", + "drops": 5 + }, + { + "oil": "安定情绪", + "drops": 8 + }, + { + "oil": "乳香", + "drops": 6 + } + ] + }, + { + "name": "卵巢囊肿2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 2 + }, + { + "oil": "温柔呵护", + "drops": 2 + }, + { + "oil": "西班牙牛至", + "drops": 1 + }, + { + "oil": "快乐鼠尾草", + "drops": 1 + } + ] + }, + { + "name": "美背", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "夏威夷檀香", + "drops": 10 + }, + { + "oil": "黑云杉", + "drops": 15 + }, + { + "oil": "雪松", + "drops": 10 + }, + { + "oil": "依兰依兰", + "drops": 10 + }, + { + "oil": "佛手柑", + "drops": 15 + }, + { + "oil": "圆柚", + "drops": 10 + }, + { + "oil": "玫瑰呵护", + "drops": 15 + }, + { + "oil": "当归", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "减轻妊娠纹", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "永久花", + "drops": 7 + }, + { + "oil": "广藿香", + "drops": 5 + }, + { + "oil": "没药", + "drops": 4 + }, + { + "oil": "乳香", + "drops": 7 + }, + { + "oil": "薰衣草", + "drops": 7 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "花样年华焕肤油", + "drops": 10 + } + ] + }, + { + "name": "更年期症", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "柠檬", + "drops": 10 + }, + { + "oil": "椒样薄荷", + "drops": 8 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "天竺葵", + "drops": 10 + }, + { + "oil": "温柔呵护", + "drops": 30 + }, + { + "oil": "快乐鼠尾草", + "drops": 20 + }, + { + "oil": "罗马洋甘菊", + "drops": 12 + } + ] + }, + { + "name": "妇科炎症", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "没药", + "drops": 10 + }, + { + "oil": "斯里兰卡肉桂皮", + "drops": 10 + }, + { + "oil": "迷迭香", + "drops": 20 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "杜松浆果", + "drops": 10 + } + ] + }, + { + "name": "备孕调理", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "温柔呵护", + "drops": 10 + }, + { + "oil": "快乐鼠尾草", + "drops": 5 + }, + { + "oil": "温柔呵护", + "drops": 10 + } + ] + }, + { + "name": "抗衰紧致", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "丝柏", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "夏威夷檀香", + "drops": 5 + }, + { + "oil": "玫瑰", + "drops": 3 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "西洋蓍草", + "drops": 20 + } + ] + }, + { + "name": "美白淡斑1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "玫瑰呵护", + "drops": 5 + }, + { + "oil": "丁香花蕾", + "drops": 2 + }, + { + "oil": "夏威夷檀香", + "drops": 5 + }, + { + "oil": "扁柏", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "岩兰草", + "drops": 5 + }, + { + "oil": "黑云杉", + "drops": 5 + }, + { + "oil": "芹菜籽", + "drops": 8 + }, + { + "oil": "花样年华焕肤油", + "drops": 10 + } + ] + }, + { + "name": "美白淡斑2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "广藿香", + "drops": 3 + }, + { + "oil": "芹菜籽", + "drops": 3 + }, + { + "oil": "黑云杉", + "drops": 3 + }, + { + "oil": "西洋蓍草", + "drops": 3 + }, + { + "oil": "橙花呵护", + "drops": 4 + }, + { + "oil": "花样年华焕肤油", + "drops": 3 + } + ] + }, + { + "name": "水油平衡", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "新清肌调理", + "drops": 15 + }, + { + "oil": "天竺葵", + "drops": 3 + }, + { + "oil": "雪松", + "drops": 5 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 3 + }, + { + "oil": "麦卢卡", + "drops": 3 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 5 + } + ] + }, + { + "name": "敏感肌", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "夏威夷檀香", + "drops": 2 + }, + { + "oil": "岩兰草", + "drops": 3 + }, + { + "oil": "广藿香", + "drops": 5 + }, + { + "oil": "罗马洋甘菊", + "drops": 10 + } + ] + }, + { + "name": "防晒修复", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗马洋甘菊", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "椒样薄荷", + "drops": 4 + }, + { + "oil": "永久花", + "drops": 8 + }, + { + "oil": "柠檬草", + "drops": 3 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "夏威夷檀香", + "drops": 5 + } + ] + }, + { + "name": "深层净化排毒", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树呵护", + "drops": 20 + }, + { + "oil": "杜松浆果", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 20 + }, + { + "oil": "圆柚", + "drops": 5 + }, + { + "oil": "蓝艾菊", + "drops": 3 + }, + { + "oil": "丝柏", + "drops": 3 + }, + { + "oil": "侧柏", + "drops": 5 + }, + { + "oil": "雪松", + "drops": 10 + } + ] + }, + { + "name": "蓝月光贵妇面霜", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "玫瑰呵护", + "drops": 10 + }, + { + "oil": "芹菜籽", + "drops": 2 + }, + { + "oil": "永久花", + "drops": 2 + }, + { + "oil": "西洋蓍草", + "drops": 10 + }, + { + "oil": "蓝艾菊", + "drops": 5 + } + ] + }, + { + "name": "唇部保养", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "夏威夷檀香", + "drops": 3 + }, + { + "oil": "没药", + "drops": 3 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 3 + }, + { + "oil": "麦卢卡", + "drops": 1 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "皱纹推土机", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "花样年华焕肤油", + "drops": 10 + }, + { + "oil": "桂花呵护", + "drops": 6 + }, + { + "oil": "麦卢卡", + "drops": 4 + }, + { + "oil": "永久花", + "drops": 4 + }, + { + "oil": "黑云杉", + "drops": 5 + }, + { + "oil": "夏威夷檀香", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "穗甘松", + "drops": 6 + }, + { + "oil": "依兰依兰", + "drops": 10 + } + ] + }, + { + "name": "祛除颈纹", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "夏威夷檀香", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "依兰依兰", + "drops": 10 + }, + { + "oil": "穗甘松", + "drops": 10 + } + ] + }, + { + "name": "蓝带神仙水", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "夏威夷檀香", + "drops": 5 + }, + { + "oil": "蓝艾菊", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "西洋蓍草", + "drops": 10 + } + ] + }, + { + "name": "全效紧致乳", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "花样年华焕肤油", + "drops": 50 + }, + { + "oil": "橙花呵护", + "drops": 50 + }, + { + "oil": "茉莉呵护", + "drops": 50 + }, + { + "oil": "古巴香脂", + "drops": 5 + }, + { + "oil": "西洋蓍草", + "drops": 5 + }, + { + "oil": "雪松", + "drops": 10 + }, + { + "oil": "玫瑰", + "drops": 5 + } + ] + }, + { + "name": "眼袋黑眼圈", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "快乐鼠尾草", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 2 + }, + { + "oil": "丝柏", + "drops": 15 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "西洋蓍草", + "drops": 5 + } + ] + }, + { + "name": "清痘无痕", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "新清肌呵护", + "drops": 20 + }, + { + "oil": "净化清新", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "花样年华焕肤油", + "drops": 10 + }, + { + "oil": "茶树", + "drops": 10 + } + ] + }, + { + "name": "脂肪粒", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "永久花", + "drops": 3 + }, + { + "oil": "西班牙牛至", + "drops": 3 + }, + { + "oil": "芫荽", + "drops": 3 + }, + { + "oil": "雪松", + "drops": 3 + }, + { + "oil": "茶树", + "drops": 3 + }, + { + "oil": "罗马洋甘菊", + "drops": 5 + } + ] + }, + { + "name": "植物蜂皮", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "橙花呵护", + "drops": 10 + }, + { + "oil": "桂花呵护", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "没药", + "drops": 3 + }, + { + "oil": "麦卢卡", + "drops": 3 + }, + { + "oil": "花样年华焕肤油", + "drops": 10 + } + ] + }, + { + "name": "植物水光针", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西洋蓍草", + "drops": 10 + }, + { + "oil": "芹菜籽", + "drops": 5 + }, + { + "oil": "玫瑰", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "木兰呵护", + "drops": 20 + } + ] + }, + { + "name": "早C精华", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西洋蓍草", + "drops": 20 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "夏威夷檀香", + "drops": 3 + }, + { + "oil": "穗甘松", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "玫瑰呵护", + "drops": 10 + } + ] + }, + { + "name": "晚A精华", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "花样年华焕肤油", + "drops": 10 + }, + { + "oil": "麦卢卡", + "drops": 3 + }, + { + "oil": "芹菜籽", + "drops": 5 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "茉莉呵护", + "drops": 10 + }, + { + "oil": "橙花呵护", + "drops": 10 + }, + { + "oil": "雪松", + "drops": 5 + } + ] + }, + { + "name": "消鸡皮肤", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "圆柚", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "枫香", + "drops": 5 + }, + { + "oil": "柠檬香桃木", + "drops": 5 + }, + { + "oil": "安定情绪", + "drops": 5 + }, + { + "oil": "天竺葵", + "drops": 6 + } + ] + }, + { + "name": "解荨麻疹", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "没药", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + }, + { + "oil": "罗马洋甘菊", + "drops": 6 + }, + { + "oil": "薰衣草", + "drops": 4 + }, + { + "oil": "蓝艾菊", + "drops": 6 + } + ] + }, + { + "name": "保湿焕肤", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "没药", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "夏威夷檀香", + "drops": 5 + }, + { + "oil": "玫瑰", + "drops": 3 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "玫瑰草", + "drops": 5 + } + ] + }, + { + "name": "奶油桂花手霜", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西洋蓍草", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "桂花", + "drops": 8 + }, + { + "oil": "薰衣草", + "drops": 8 + }, + { + "oil": "天竺葵", + "drops": 8 + }, + { + "oil": "椰风香草", + "drops": 8 + } + ] + }, + { + "name": "养护指甲", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "夏威夷檀香", + "drops": 10 + }, + { + "oil": "罗马洋甘菊", + "drops": 10 + }, + { + "oil": "没药", + "drops": 10 + } + ] + }, + { + "name": "皮肤皲裂/脚后跟干裂", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "天竺葵", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 8 + }, + { + "oil": "没药", + "drops": 8 + } + ] + }, + { + "name": "黑绷带", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "广藿香", + "drops": 6 + }, + { + "oil": "岩兰草", + "drops": 6 + }, + { + "oil": "穗甘松", + "drops": 6 + }, + { + "oil": "夏威夷檀香", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 10 + }, + { + "oil": "没药", + "drops": 10 + }, + { + "oil": "蓝艾菊", + "drops": 10 + } + ] + }, + { + "name": "眉毛睫毛增长滋养液", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "迷迭香", + "drops": 8 + }, + { + "oil": "雪松", + "drops": 8 + }, + { + "oil": "依兰依兰", + "drops": 6 + }, + { + "oil": "薰衣草", + "drops": 8 + } + ] + }, + { + "name": "天鹅颈", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "穗甘松", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "舒缓", + "drops": 10 + }, + { + "oil": "依兰依兰", + "drops": 10 + }, + { + "oil": "丝柏", + "drops": 10 + }, + { + "oil": "芳香调理", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "收缩毛孔", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "迷迭香", + "drops": 5 + }, + { + "oil": "依兰依兰", + "drops": 7 + }, + { + "oil": "天竺葵", + "drops": 10 + }, + { + "oil": "丝柏", + "drops": 10 + } + ] + }, + { + "name": "酒糟鼻", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 7 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "岩兰草", + "drops": 8 + }, + { + "oil": "罗马洋甘菊", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 8 + } + ] + }, + { + "name": "疤痕修复", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "没药", + "drops": 4 + }, + { + "oil": "乳香", + "drops": 7 + }, + { + "oil": "薰衣草", + "drops": 7 + }, + { + "oil": "永久花", + "drops": 7 + }, + { + "oil": "花样年华焕肤油", + "drops": 10 + } + ] + }, + { + "name": "太阳油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "依兰依兰", + "drops": 15 + }, + { + "oil": "广藿香", + "drops": 15 + }, + { + "oil": "雪松", + "drops": 15 + }, + { + "oil": "檀香", + "drops": 15 + }, + { + "oil": "杜松浆果", + "drops": 15 + } + ] + }, + { + "name": "月亮油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "温柔呵护", + "drops": 15 + }, + { + "oil": "茉莉呵护", + "drops": 10 + }, + { + "oil": "玫瑰呵护", + "drops": 10 + }, + { + "oil": "佛手柑", + "drops": 5 + }, + { + "oil": "小茴香", + "drops": 5 + }, + { + "oil": "依兰依兰", + "drops": 7 + }, + { + "oil": "快乐鼠尾草", + "drops": 10 + } + ] + }, + { + "name": "阿育吠陀", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "姜黄", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "杜松浆果", + "drops": 10 + }, + { + "oil": "苦橙叶", + "drops": 10 + }, + { + "oil": "迷迭香", + "drops": 5 + }, + { + "oil": "柠檬", + "drops": 10 + }, + { + "oil": "圆柚", + "drops": 5 + } + ] + }, + { + "name": "缓解头痛", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 5 + }, + { + "oil": "马郁兰", + "drops": 7 + }, + { + "oil": "舒缓", + "drops": 15 + } + ] + }, + { + "name": "白发变黑(女士)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 7 + }, + { + "oil": "快乐鼠尾草", + "drops": 10 + }, + { + "oil": "依兰依兰", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 7 + }, + { + "oil": "薰衣草", + "drops": 5 + } + ] + }, + { + "name": "白发变黑(男士)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "迷迭香", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "雪松", + "drops": 10 + }, + { + "oil": "永久花", + "drops": 5 + } + ] + }, + { + "name": "头屑清爽", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "雪松", + "drops": 20 + }, + { + "oil": "迷迭香", + "drops": 10 + }, + { + "oil": "丝柏", + "drops": 15 + }, + { + "oil": "茶树", + "drops": 20 + } + ] + }, + { + "name": "口腔溃疡1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "丁香花蕾", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "没药", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "口腔溃疡2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 10 + }, + { + "oil": "麦卢卡", + "drops": 10 + }, + { + "oil": "月桂叶", + "drops": 10 + }, + { + "oil": "佛手柑", + "drops": 20 + }, + { + "oil": "薰衣草", + "drops": 20 + } + ] + }, + { + "name": "扁桃腺炎", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "百里香", + "drops": 5 + }, + { + "oil": "小豆蔻", + "drops": 5 + }, + { + "oil": "尤加利", + "drops": 5 + }, + { + "oil": "保卫", + "drops": 5 + }, + { + "oil": "顺畅呼吸", + "drops": 5 + } + ] + }, + { + "name": "慢性阑尾炎", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "西班牙牛至", + "drops": 3 + }, + { + "oil": "保卫", + "drops": 3 + }, + { + "oil": "乐活", + "drops": 10 + }, + { + "oil": "新瑞活力", + "drops": 5 + }, + { + "oil": "完美修护", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "结石、胆囊炎", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗勒", + "drops": 5 + }, + { + "oil": "冬青", + "drops": 5 + }, + { + "oil": "柠檬", + "drops": 10 + }, + { + "oil": "天竺葵", + "drops": 5 + }, + { + "oil": "迷迭香", + "drops": 5 + }, + { + "oil": "圆柚", + "drops": 10 + }, + { + "oil": "莱姆", + "drops": 10 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "脚气、灰指甲", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "香蜂草", + "drops": 3 + }, + { + "oil": "保卫", + "drops": 7 + }, + { + "oil": "茶树", + "drops": 10 + }, + { + "oil": "西班牙牛至", + "drops": 10 + }, + { + "oil": "广藿香", + "drops": 5 + } + ] + }, + { + "name": "甲亢、甲减", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 9 + }, + { + "oil": "丁香花蕾", + "drops": 12 + }, + { + "oil": "柠檬草", + "drops": 15 + }, + { + "oil": "没药", + "drops": 12 + }, + { + "oil": "乳香", + "drops": 12 + } + ] + }, + { + "name": "降压仪式", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "依兰依兰", + "drops": 10 + }, + { + "oil": "香蜂草", + "drops": 8 + }, + { + "oil": "马郁兰", + "drops": 15 + }, + { + "oil": "乳香", + "drops": 15 + }, + { + "oil": "薰衣草", + "drops": 12 + } + ] + }, + { + "name": "伤筋动骨", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "完美修护", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "生姜", + "drops": 10 + }, + { + "oil": "西班牙牛至", + "drops": 10 + }, + { + "oil": "冬青", + "drops": 15 + }, + { + "oil": "柠檬草", + "drops": 15 + }, + { + "oil": "舒缓", + "drops": 5 + } + ] + }, + { + "name": "干眼症", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "薰衣草", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "罗马洋甘菊", + "drops": 5 + }, + { + "oil": "快乐鼠尾草", + "drops": 10 + }, + { + "oil": "古巴香脂", + "drops": 5 + }, + { + "oil": "完美修护", + "drops": 5 + } + ] + }, + { + "name": "儿童抚触", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 3 + }, + { + "oil": "保卫", + "drops": 3 + }, + { + "oil": "西伯利亚冷杉", + "drops": 5 + }, + { + "oil": "乐活", + "drops": 5 + }, + { + "oil": "没药", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "儿童脾胃", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "红橘", + "drops": 10 + }, + { + "oil": "小茴香", + "drops": 2 + }, + { + "oil": "生姜", + "drops": 3 + }, + { + "oil": "乐活", + "drops": 5 + } + ] + }, + { + "name": "视力养护", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 3 + }, + { + "oil": "柠檬草", + "drops": 3 + }, + { + "oil": "永久花", + "drops": 2 + }, + { + "oil": "快乐鼠尾草", + "drops": 5 + } + ] + }, + { + "name": "驱蚊喷雾", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "柠檬尤加利", + "drops": 5 + }, + { + "oil": "尤加利", + "drops": 5 + }, + { + "oil": "香茅", + "drops": 5 + }, + { + "oil": "天竺葵", + "drops": 3 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "天然防护", + "drops": 20 + } + ] + }, + { + "name": "个子高高", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "檀香", + "drops": 3 + }, + { + "oil": "永久花", + "drops": 3 + }, + { + "oil": "芳香调理", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "西伯利亚冷杉", + "drops": 5 + } + ] + }, + { + "name": "清咽止咳", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "西伯利亚冷杉", + "drops": 3 + }, + { + "oil": "尤加利", + "drops": 3 + }, + { + "oil": "保卫", + "drops": 2 + }, + { + "oil": "顺畅呼吸", + "drops": 5 + } + ] + }, + { + "name": "通鼻消炎", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 2 + }, + { + "oil": "椒样薄荷", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 3 + }, + { + "oil": "蓝艾菊", + "drops": 1 + }, + { + "oil": "尤加利", + "drops": 3 + }, + { + "oil": "迷迭香", + "drops": 2 + }, + { + "oil": "顺畅呼吸", + "drops": 5 + } + ] + }, + { + "name": "蚊虫叮咬1", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 3 + }, + { + "oil": "薰衣草", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 2 + }, + { + "oil": "罗勒", + "drops": 3 + }, + { + "oil": "天然防护", + "drops": 5 + } + ] + }, + { + "name": "蚊虫叮咬2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "香茅", + "drops": 5 + }, + { + "oil": "柠檬草", + "drops": 5 + }, + { + "oil": "天然防护", + "drops": 10 + } + ] + }, + { + "name": "学霸神助", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "檀香", + "drops": 2 + }, + { + "oil": "完美修护", + "drops": 2 + }, + { + "oil": "椒样薄荷", + "drops": 4 + }, + { + "oil": "全神贯注", + "drops": 8 + }, + { + "oil": "罗勒", + "drops": 2 + }, + { + "oil": "迷迭香", + "drops": 4 + }, + { + "oil": "乳香", + "drops": 4 + } + ] + }, + { + "name": "磨牙安抚", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "佛手柑", + "drops": 5 + }, + { + "oil": "罗勒", + "drops": 5 + }, + { + "oil": "芳香调理", + "drops": 5 + }, + { + "oil": "古巴香脂", + "drops": 5 + }, + { + "oil": "安定情绪", + "drops": 5 + } + ] + }, + { + "name": "免疫助力", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "野橘", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 5 + }, + { + "oil": "侧柏", + "drops": 3 + }, + { + "oil": "姜黄", + "drops": 3 + }, + { + "oil": "保卫", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "腺样体肥大", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 3 + }, + { + "oil": "百里香", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 3 + }, + { + "oil": "尤加利", + "drops": 2 + }, + { + "oil": "雪松", + "drops": 3 + }, + { + "oil": "顺畅呼吸", + "drops": 5 + } + ] + }, + { + "name": "退烧方案", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "顺畅呼吸", + "drops": 3 + }, + { + "oil": "保卫", + "drops": 5 + }, + { + "oil": "椒样薄荷", + "drops": 5 + }, + { + "oil": "茶树", + "drops": 3 + }, + { + "oil": "薰衣草", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "手足口症", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 3 + }, + { + "oil": "香蜂草", + "drops": 3 + }, + { + "oil": "丁香花蕾", + "drops": 3 + }, + { + "oil": "月桂叶", + "drops": 3 + }, + { + "oil": "保卫", + "drops": 3 + }, + { + "oil": "古巴香脂", + "drops": 3 + } + ] + }, + { + "name": "湿疹修复", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "蓝艾菊", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 3 + }, + { + "oil": "天竺葵", + "drops": 3 + }, + { + "oil": "广藿香", + "drops": 3 + }, + { + "oil": "罗马洋甘菊", + "drops": 5 + } + ] + }, + { + "name": "抽动症", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗马洋甘菊", + "drops": 3 + }, + { + "oil": "安定情绪", + "drops": 5 + }, + { + "oil": "雪松", + "drops": 3 + }, + { + "oil": "岩兰草", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 5 + } + ] + }, + { + "name": "头疗生发", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 10 + }, + { + "oil": "茶树", + "drops": 10 + }, + { + "oil": "迷迭香", + "drops": 10 + }, + { + "oil": "丝柏", + "drops": 20 + }, + { + "oil": "生姜", + "drops": 10 + }, + { + "oil": "雪松", + "drops": 20 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 14 + }, + { + "oil": "安定情绪", + "drops": 14 + } + ] + }, + { + "name": "1、呼吸系统细胞律动(含香蜂草)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "茶树", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "顺畅呼吸", + "drops": 4 + }, + { + "oil": "迷迭香", + "drops": 4 + }, + { + "oil": "尤加利", + "drops": 4 + }, + { + "oil": "香蜂草", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "1、呼吸系统细胞律动(不含香蜂草)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "茶树", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "顺畅呼吸", + "drops": 4 + }, + { + "oil": "迷迭香", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "2、神经系统细胞律动", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "百里香", + "drops": 4 + }, + { + "oil": "丁香花蕾", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "柠檬草", + "drops": 4 + }, + { + "oil": "香蜂草", + "drops": 4 + }, + { + "oil": "广藿香", + "drops": 4 + }, + { + "oil": "佛手柑", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "3、消化系统细胞律动", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "百里香", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "佛手柑", + "drops": 4 + }, + { + "oil": "芫荽", + "drops": 4 + }, + { + "oil": "乐活", + "drops": 4 + }, + { + "oil": "生姜", + "drops": 4 + }, + { + "oil": "天竺葵", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "4、骨骼系统细胞律动(炎症控制)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "茶树", + "drops": 4 + }, + { + "oil": "冬青", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "柠檬草", + "drops": 4 + }, + { + "oil": "西伯利亚冷杉", + "drops": 4 + }, + { + "oil": "舒缓", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "5、淋巴系统细胞律动", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "迷迭香", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "柠檬草", + "drops": 4 + }, + { + "oil": "新瑞活力", + "drops": 4 + }, + { + "oil": "元气", + "drops": 4 + }, + { + "oil": "柠檬", + "drops": 4 + }, + { + "oil": "圆柚", + "drops": 4 + }, + { + "oil": "生姜", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "6、生殖系统细胞律动", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "西班牙牛至", + "drops": 4 + }, + { + "oil": "茶树", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "薰衣草", + "drops": 4 + }, + { + "oil": "广藿香", + "drops": 4 + }, + { + "oil": "芫荽", + "drops": 4 + }, + { + "oil": "快乐鼠尾草", + "drops": 4 + }, + { + "oil": "檀香", + "drops": 4 + }, + { + "oil": "丁香花蕾", + "drops": 4 + }, + { + "oil": "依兰依兰", + "drops": 4 + }, + { + "oil": "天竺葵", + "drops": 4 + }, + { + "oil": "温柔呵护", + "drops": 4 + }, + { + "oil": "元气", + "drops": 4 + }, + { + "oil": "温柔呵护", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "7、免疫系统细胞律动", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "西班牙牛至", + "drops": 4 + }, + { + "oil": "茶树", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "西伯利亚冷杉", + "drops": 4 + }, + { + "oil": "丝柏", + "drops": 4 + }, + { + "oil": "柠檬", + "drops": 4 + }, + { + "oil": "莱姆", + "drops": 4 + }, + { + "oil": "圆柚", + "drops": 4 + }, + { + "oil": "保卫", + "drops": 4 + }, + { + "oil": "丁香花蕾", + "drops": 4 + }, + { + "oil": "百里香", + "drops": 4 + }, + { + "oil": "元气", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "8、循环系统细胞律动", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "百里香", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "柠檬草", + "drops": 4 + }, + { + "oil": "保卫", + "drops": 4 + }, + { + "oil": "马郁兰", + "drops": 4 + }, + { + "oil": "罗勒", + "drops": 4 + }, + { + "oil": "薰衣草", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "9、内分泌系统细胞律动", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "快乐鼠尾草", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "天竺葵", + "drops": 4 + }, + { + "oil": "保卫", + "drops": 4 + }, + { + "oil": "依兰依兰", + "drops": 4 + }, + { + "oil": "小茴香", + "drops": 4 + }, + { + "oil": "薰衣草", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "10、感冒发烧系统细胞律动", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "西班牙牛至", + "drops": 4 + }, + { + "oil": "百里香", + "drops": 4 + }, + { + "oil": "保卫", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "柠檬草", + "drops": 4 + }, + { + "oil": "尤加利", + "drops": 4 + }, + { + "oil": "茶树", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "11、肌肉系统细胞律动(缓解疼痛)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "生姜", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "冬青", + "drops": 4 + }, + { + "oil": "柠檬草", + "drops": 4 + }, + { + "oil": "西伯利亚冷杉", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "12、芳香调理技术", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "安定情绪", + "drops": 4 + }, + { + "oil": "薰衣草", + "drops": 4 + }, + { + "oil": "茶树", + "drops": 4 + }, + { + "oil": "保卫", + "drops": 4 + }, + { + "oil": "芳香调理", + "drops": 4 + }, + { + "oil": "舒缓", + "drops": 4 + }, + { + "oil": "野橘", + "drops": 4 + }, + { + "oil": "椒样薄荷", + "drops": 4 + } + ] + }, + { + "name": "芳心调理技术(单瓶购买)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "静谧", + "drops": 4 + }, + { + "oil": "抚慰", + "drops": 4 + }, + { + "oil": "宽容", + "drops": 4 + }, + { + "oil": "热情", + "drops": 4 + }, + { + "oil": "欢欣", + "drops": 4 + }, + { + "oil": "鼓舞", + "drops": 4 + } + ] + }, + { + "name": "芳心调理技术(套装购买)", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "静谧", + "drops": 4 + }, + { + "oil": "抚慰", + "drops": 4 + }, + { + "oil": "宽容", + "drops": 4 + }, + { + "oil": "热情", + "drops": 4 + }, + { + "oil": "欢欣", + "drops": 4 + }, + { + "oil": "鼓舞", + "drops": 4 + } + ] + }, + { + "name": "头疼", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "尤加利", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 1 + }, + { + "oil": "冬青", + "drops": 2 + }, + { + "oil": "椒样薄荷", + "drops": 2 + } + ] + }, + { + "name": "痤疮粉刺", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 1 + } + ] + }, + { + "name": "植物热玛吉", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "玫瑰", + "drops": 5 + }, + { + "oil": "丝柏", + "drops": 10 + }, + { + "oil": "西洋蓍草", + "drops": 20 + }, + { + "oil": "永久花", + "drops": 5 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "近视老花", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "快乐鼠尾草", + "drops": 10 + }, + { + "oil": "花样年华焕肤油", + "drops": 10 + } + ] + }, + { + "name": "记忆力", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "罗勒", + "drops": 1 + }, + { + "oil": "迷迭香", + "drops": 2 + }, + { + "oil": "佛手柑", + "drops": 2 + } + ] + }, + { + "name": "皮肤老化", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "花样年华焕肤油", + "drops": 3 + }, + { + "oil": "西洋蓍草", + "drops": 3 + } + ] + }, + { + "name": "生发配方", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 1 + }, + { + "oil": "迷迭香", + "drops": 1 + }, + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "雪松", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 1 + }, + { + "oil": "扁柏", + "drops": 1 + } + ] + }, + { + "name": "头皮屑", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "丝柏", + "drops": 1 + }, + { + "oil": "迷迭香", + "drops": 1 + } + ] + }, + { + "name": "生发2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 2 + }, + { + "oil": "迷迭香", + "drops": 3 + }, + { + "oil": "雪松", + "drops": 4 + } + ] + }, + { + "name": "白发转黑发", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "完美修护", + "drops": 16 + }, + { + "oil": "乳香", + "drops": 16 + }, + { + "oil": "雪松", + "drops": 16 + }, + { + "oil": "薰衣草", + "drops": 16 + }, + { + "oil": "丝柏", + "drops": 16 + }, + { + "oil": "快乐鼠尾草", + "drops": 16 + }, + { + "oil": "生姜", + "drops": 16 + }, + { + "oil": "侧柏", + "drops": 16 + }, + { + "oil": "扁柏", + "drops": 16 + }, + { + "oil": "依兰依兰", + "drops": 16 + }, + { + "oil": "迷迭香", + "drops": 16 + }, + { + "oil": "檀香", + "drops": 10 + }, + { + "oil": "西伯利亚冷杉", + "drops": 10 + } + ] + }, + { + "name": "鼻炎用油", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "香蜂草", + "drops": 4 + }, + { + "oil": "顺畅呼吸", + "drops": 20 + }, + { + "oil": "椒样薄荷", + "drops": 4 + }, + { + "oil": "罗勒", + "drops": 6 + }, + { + "oil": "尤加利", + "drops": 6 + } + ] + }, + { + "name": "中耳炎", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 1 + }, + { + "oil": "罗勒", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 2 + }, + { + "oil": "薰衣草", + "drops": 2 + } + ] + }, + { + "name": "口臭", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "绿薄荷", + "drops": 1 + } + ] + }, + { + "name": "牙龈牙周", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "丁香花蕾", + "drops": 1 + } + ] + }, + { + "name": "烧烫伤", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 2 + }, + { + "oil": "茶树", + "drops": 2 + }, + { + "oil": "薰衣草", + "drops": 2 + } + ] + }, + { + "name": "儿童长高", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西伯利亚冷杉", + "drops": 6 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "檀香", + "drops": 3 + }, + { + "oil": "永久花", + "drops": 3 + }, + { + "oil": "芳香调理", + "drops": 2 + } + ] + }, + { + "name": "湿疹", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "绿薄荷", + "drops": 1 + }, + { + "oil": "侧柏", + "drops": 5 + }, + { + "oil": "蓝艾菊", + "drops": 5 + }, + { + "oil": "广藿香", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + } + ] + }, + { + "name": "退烧神器", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "杜松浆果", + "drops": 3 + }, + { + "oil": "椒样薄荷", + "drops": 4 + }, + { + "oil": "永久花", + "drops": 3 + }, + { + "oil": "山鸡椒", + "drops": 3 + }, + { + "oil": "保卫", + "drops": 4 + }, + { + "oil": "柠檬", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 4 + } + ] + }, + { + "name": "感冒咳嗽", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 1 + }, + { + "oil": "柠檬", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 3 + }, + { + "oil": "西班牙牛至", + "drops": 4 + }, + { + "oil": "保卫", + "drops": 5 + } + ] + }, + { + "name": "腹泻", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西班牙牛至", + "drops": 1 + }, + { + "oil": "乐活", + "drops": 1 + }, + { + "oil": "生姜", + "drops": 1 + }, + { + "oil": "罗勒", + "drops": 1 + } + ] + }, + { + "name": "晕车恶心", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 1 + }, + { + "oil": "尤加利", + "drops": 1 + }, + { + "oil": "佛手柑", + "drops": 1 + }, + { + "oil": "椒样薄荷", + "drops": 1 + }, + { + "oil": "乐活", + "drops": 1 + } + ] + }, + { + "name": "醉酒", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乐活", + "drops": 2 + } + ] + }, + { + "name": "鸡眼/疣/痣", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "丁香花蕾", + "drops": 1 + }, + { + "oil": "西班牙牛至", + "drops": 1 + } + ] + }, + { + "name": "脚气", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "西班牙牛至", + "drops": 1 + } + ] + }, + { + "name": "灰指甲2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 8 + }, + { + "oil": "西班牙牛至", + "drops": 8 + }, + { + "oil": "百里香", + "drops": 8 + } + ] + }, + { + "name": "丰胸挺拔", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "小茴香", + "drops": 6 + }, + { + "oil": "依兰依兰", + "drops": 4 + }, + { + "oil": "温柔呵护", + "drops": 10 + }, + { + "oil": "西洋蓍草", + "drops": 10 + }, + { + "oil": "快乐鼠尾草", + "drops": 3 + } + ] + }, + { + "name": "痛经", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "斯里兰卡肉桂皮", + "drops": 1 + }, + { + "oil": "玫瑰", + "drops": 1 + }, + { + "oil": "丁香花蕾", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 2 + } + ] + }, + { + "name": "亲密关系", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "快乐鼠尾草", + "drops": 6 + }, + { + "oil": "茉莉呵护", + "drops": 12 + }, + { + "oil": "檀香", + "drops": 7 + }, + { + "oil": "依兰依兰", + "drops": 6 + }, + { + "oil": "花样年华焕肤油", + "drops": 5 + }, + { + "oil": "玫瑰呵护", + "drops": 12 + } + ] + }, + { + "name": "前列腺养护", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "西班牙牛至", + "drops": 2 + }, + { + "oil": "檀香", + "drops": 3 + }, + { + "oil": "茉莉呵护", + "drops": 4 + }, + { + "oil": "丝柏", + "drops": 3 + }, + { + "oil": "乳香", + "drops": 4 + }, + { + "oil": "永久花", + "drops": 3 + }, + { + "oil": "迷迭香", + "drops": 3 + }, + { + "oil": "快乐鼠尾草", + "drops": 4 + } + ] + }, + { + "name": "手脚冰冷", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "生姜", + "drops": 2 + }, + { + "oil": "圆柚", + "drops": 2 + } + ] + }, + { + "name": "外阴瘙痒", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "没药", + "drops": 1 + }, + { + "oil": "玫瑰草", + "drops": 1 + }, + { + "oil": "佛手柑", + "drops": 1 + } + ] + }, + { + "name": "淋巴排毒2", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "丝柏", + "drops": 1 + }, + { + "oil": "迷迭香", + "drops": 1 + }, + { + "oil": "柠檬草", + "drops": 1 + }, + { + "oil": "永久花", + "drops": 1 + }, + { + "oil": "新瑞活力", + "drops": 1 + } + ] + }, + { + "name": "痔疮", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "没药", + "drops": 1 + }, + { + "oil": "茶树", + "drops": 1 + }, + { + "oil": "丝柏", + "drops": 2 + }, + { + "oil": "永久花", + "drops": 2 + } + ] + }, + { + "name": "便秘积食", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乐活", + "drops": 1 + }, + { + "oil": "柠檬", + "drops": 1 + }, + { + "oil": "马郁兰", + "drops": 1 + }, + { + "oil": "椒样薄荷", + "drops": 1 + } + ] + }, + { + "name": "护肝排毒", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "当归", + "drops": 1 + }, + { + "oil": "芫荽", + "drops": 2 + }, + { + "oil": "元气", + "drops": 3 + }, + { + "oil": "天竺葵", + "drops": 3 + }, + { + "oil": "迷迭香", + "drops": 7 + }, + { + "oil": "柠檬", + "drops": 13 + } + ] + }, + { + "name": "痛风", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "百里香", + "drops": 6 + }, + { + "oil": "杜松浆果", + "drops": 8 + }, + { + "oil": "天竺葵", + "drops": 10 + }, + { + "oil": "冬青", + "drops": 10 + } + ] + }, + { + "name": "消除富贵包", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "柠檬草", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 5 + }, + { + "oil": "芳香调理", + "drops": 5 + }, + { + "oil": "生姜", + "drops": 2 + }, + { + "oil": "舒缓", + "drops": 3 + }, + { + "oil": "古巴香脂", + "drops": 2 + } + ] + }, + { + "name": "焦虑", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "野橘", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 2 + }, + { + "oil": "天竺葵", + "drops": 2 + } + ] + }, + { + "name": "更年期", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "椒样薄荷", + "drops": 2 + }, + { + "oil": "乳香", + "drops": 3 + }, + { + "oil": "薰衣草", + "drops": 3 + }, + { + "oil": "天竺葵", + "drops": 4 + }, + { + "oil": "温柔呵护", + "drops": 8 + }, + { + "oil": "快乐鼠尾草", + "drops": 10 + }, + { + "oil": "罗马洋甘菊", + "drops": 8 + } + ] + }, + { + "name": "失眠多梦", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "雪松", + "drops": 1 + }, + { + "oil": "野橘", + "drops": 1 + }, + { + "oil": "乳香", + "drops": 1 + }, + { + "oil": "檀香", + "drops": 1 + }, + { + "oil": "橙花", + "drops": 1 + }, + { + "oil": "乐释", + "drops": 1 + }, + { + "oil": "苦橙叶", + "drops": 1 + }, + { + "oil": "薰衣草", + "drops": 1 + }, + { + "oil": "马郁兰", + "drops": 1 + }, + { + "oil": "佛手柑", + "drops": 1 + }, + { + "oil": "岩兰草", + "drops": 1 + }, + { + "oil": "罗马洋甘菊", + "drops": 1 + } + ] + }, + { + "name": "稳定血糖", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "新瑞活力", + "drops": 2 + }, + { + "oil": "斯里兰卡肉桂皮", + "drops": 2 + }, + { + "oil": "芫荽", + "drops": 2 + }, + { + "oil": "迷迭香", + "drops": 2 + } + ] + }, + { + "name": "心脑血管护理", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 2 + }, + { + "oil": "香蜂草", + "drops": 1 + } + ] + }, + { + "name": "关节疼痛", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "乳香", + "drops": 3 + }, + { + "oil": "舒缓", + "drops": 2 + }, + { + "oil": "道格拉斯冷杉", + "drops": 2 + }, + { + "oil": "柠檬草", + "drops": 1 + } + ] + }, + { + "name": "高血压保健", + "note": "", + "tags": [], + "ingredients": [ + { + "oil": "依兰依兰", + "drops": 10 + }, + { + "oil": "薰衣草", + "drops": 10 + }, + { + "oil": "马郁兰", + "drops": 10 + }, + { + "oil": "乳香", + "drops": 10 + }, + { + "oil": "香蜂草", + "drops": 5 + } + ] + } + ] +} \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..d9e0e0b --- /dev/null +++ b/backend/main.py @@ -0,0 +1,1497 @@ +from fastapi import FastAPI, HTTPException, Request, Depends +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel +from typing import Optional +import json +import os + +from backend.database import get_db, init_db, seed_defaults, log_audit + +app = FastAPI(title="Essential Oil Formula Calculator API") + +# Periodic WAL checkpoint to ensure data is flushed to main DB file +import threading, time as _time +def _wal_checkpoint_loop(): + while True: + _time.sleep(300) # Every 5 minutes + try: + conn = get_db() + conn.execute("PRAGMA wal_checkpoint(TRUNCATE)") + conn.close() + except: pass +threading.Thread(target=_wal_checkpoint_loop, daemon=True).start() + + +# ── Auth ──────────────────────────────────────────────── +ANON_USER = {"id": None, "role": "viewer", "username": "anonymous", "display_name": "匿名用户"} + + +def get_current_user(request: Request): + token = request.headers.get("Authorization", "").removeprefix("Bearer ").strip() + if not token: + return ANON_USER + conn = get_db() + user = conn.execute("SELECT id, username, role, display_name, password, business_verified FROM users WHERE token = ?", (token,)).fetchone() + conn.close() + if not user: + return ANON_USER + return dict(user) + + +def require_role(*roles): + """Returns a dependency that checks the user has one of the given roles.""" + def checker(user=Depends(get_current_user)): + if user["role"] not in roles: + raise HTTPException(403, "权限不足") + return user + return checker + + +# ── Models ────────────────────────────────────────────── +class OilIn(BaseModel): + name: str + bottle_price: float + drop_count: int + retail_price: Optional[float] = None + + +class IngredientIn(BaseModel): + oil_name: str + drops: float + + +class RecipeIn(BaseModel): + name: str + note: str = "" + ingredients: list[IngredientIn] + tags: list[str] = [] + + +class RecipeUpdate(BaseModel): + name: Optional[str] = None + note: Optional[str] = None + ingredients: Optional[list[IngredientIn]] = None + tags: Optional[list[str]] = None + version: Optional[int] = None + + +class UserIn(BaseModel): + username: str + role: str = "viewer" + display_name: str = "" + + +class UserUpdate(BaseModel): + role: Optional[str] = None + display_name: Optional[str] = None + + +# ── Me ────────────────────────────────────────────────── +APP_VERSION = "20260401" + +@app.get("/api/version") +def get_version(): + return {"version": APP_VERSION} + + +@app.get("/api/me") +def get_me(user=Depends(get_current_user)): + return {"username": user["username"], "role": user["role"], "display_name": user.get("display_name", ""), "id": user.get("id"), "has_password": bool(user.get("password")), "business_verified": bool(user.get("business_verified"))} + + +# ── Bug Reports ───────────────────────────────────────── +@app.post("/api/bug-report", status_code=201) +def submit_bug(body: dict, user=Depends(get_current_user)): + content = body.get("content", "").strip() + if not content: + raise HTTPException(400, "请输入内容") + conn = get_db() + who = user.get("display_name") or user.get("username") or "匿名" + # Admin can set priority; user-submitted bugs default to urgent (0) + default_pri = 2 if user.get("role") == "admin" else 0 + priority = body.get("priority", default_pri) + c = conn.execute("INSERT INTO bug_reports (user_id, content, priority) VALUES (?, ?, ?)", (user.get("id"), content, priority)) + bug_id = c.lastrowid + # Auto-log: created + conn.execute( + "INSERT INTO bug_comments (bug_id, user_id, action, content) VALUES (?, ?, ?, ?)", + (bug_id, user.get("id"), "创建", content), + ) + # Only notify admin if submitter is NOT admin + if user.get("role") != "admin": + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + ("admin", "🐛 Bug 反馈", f"{who}:{content}\n[bug_id:{bug_id}]") + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.get("/api/bug-reports") +def list_bugs(user=Depends(get_current_user)): + conn = get_db() + if user["role"] == "admin": + rows = conn.execute( + "SELECT b.id, b.content, b.is_resolved, b.created_at, b.user_id, b.priority, b.assigned_to, " + "u.display_name, u.username, a.display_name as assigned_name " + "FROM bug_reports b LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users a ON b.assigned_to = a.id " + "ORDER BY b.priority ASC, COALESCE((SELECT MAX(c.created_at) FROM bug_comments c WHERE c.bug_id=b.id), b.created_at) DESC" + ).fetchall() + else: + rows = conn.execute( + "SELECT b.id, b.content, b.is_resolved, b.created_at, b.user_id, b.priority, b.assigned_to, " + "u.display_name, u.username, a.display_name as assigned_name " + "FROM bug_reports b LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users a ON b.assigned_to = a.id " + "WHERE b.user_id = ? OR b.assigned_to = ? OR b.is_resolved IN (1, 3) " + "ORDER BY b.priority ASC, COALESCE((SELECT MAX(c.created_at) FROM bug_comments c WHERE c.bug_id=b.id), b.created_at) DESC", + (user.get("id"), user.get("id")) + ).fetchall() + bugs = [dict(r) for r in rows] + # Attach comments/log for each bug + for bug in bugs: + comments = conn.execute( + "SELECT c.id, c.action, c.content, c.created_at, u.display_name, u.username " + "FROM bug_comments c LEFT JOIN users u ON c.user_id = u.id " + "WHERE c.bug_id = ? ORDER BY c.created_at ASC", + (bug["id"],) + ).fetchall() + bug["comments"] = [dict(c) for c in comments] + conn.close() + return bugs + + +@app.put("/api/bug-reports/{bug_id}") +def update_bug(bug_id: int, body: dict, user=Depends(get_current_user)): + conn = get_db() + bug = conn.execute("SELECT user_id, content, is_resolved FROM bug_reports WHERE id = ?", (bug_id,)).fetchone() + if not bug: + conn.close() + raise HTTPException(404, "Bug not found") + + # status: 0=open, 1=待测试, 2=已修复(admin only), 3=已测试(tester feedback) + status_names = {0: "待处理", 1: "待测试", 2: "已修复", 3: "已测试"} + new_status = body.get("status") + note = body.get("note", "").strip() + + if new_status is not None: + # Only admin can mark as resolved (status 2) + if new_status == 2 and user["role"] != "admin": + conn.close() + raise HTTPException(403, "只有管理员可以标记为已修复") + + conn.execute("UPDATE bug_reports SET is_resolved = ? WHERE id = ?", (new_status, bug_id)) + + # Auto-log the status change + action = "→ " + status_names.get(new_status, str(new_status)) + conn.execute( + "INSERT INTO bug_comments (bug_id, user_id, action, content) VALUES (?, ?, ?, ?)", + (bug_id, user.get("id"), action, note), + ) + + notify_user_id = body.get("notify_user_id") + if new_status == 1 and notify_user_id: + # Admin sends to specific tester — user-targeted notification + conn.execute("UPDATE bug_reports SET assigned_to = ? WHERE id = ?", (notify_user_id, bug_id)) + target = conn.execute("SELECT role, display_name, username FROM users WHERE id = ?", (notify_user_id,)).fetchone() + if target: + msg = "请帮忙测试「" + bug["content"][:80] + "」" + if note: + msg += "\n\n备注:" + note + msg += "\n[bug_id:" + str(bug_id) + "]" + conn.execute( + "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)", + (target["role"], "🔧 Bug 待测试", msg, notify_user_id) + ) + elif new_status == 1 and bug["user_id"]: + # Admin marks as testing → notify reporter + reporter = conn.execute("SELECT role, display_name, username FROM users WHERE id = ?", (bug["user_id"],)).fetchone() + if reporter: + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + (reporter["role"], "🔧 你的 Bug 已修复,请测试", + "请帮忙测试「" + bug["content"][:80] + "」" + ("\n\n备注:" + note if note else "")) + ) + if reporter["role"] in ("viewer", "editor"): + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + ("senior_editor", "🔧 Bug 待测试(来自" + (reporter["display_name"] or reporter["username"]) + ")", + "问题「" + bug["content"][:50] + "」已修复,请协助测试确认。") + ) + elif new_status == 3: + # Tester confirms tested → notify admin + who = user.get("display_name") or user.get("username") or "测试者" + msg = who + " 已测试「" + bug["content"][:50] + "」" + if note: + msg += "\n\n备注:" + note + msg += "\n[bug_id:" + str(bug_id) + "]" + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + ("admin", "🧪 Bug 已测试", msg) + ) + elif new_status == 2: + # Admin marks as resolved + pass # No notification needed, admin did it themselves + + if "content" in body: + conn.execute("UPDATE bug_reports SET content = ? WHERE id = ?", (body["content"], bug_id)) + if "priority" in body: + conn.execute("UPDATE bug_reports SET priority = ? WHERE id = ?", (body["priority"], bug_id)) + + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/bug-reports/{bug_id}/comment") +@app.delete("/api/bug-reports/{bug_id}") +def delete_bug(bug_id: int, user=Depends(require_role("admin"))): + conn = get_db() + conn.execute("DELETE FROM bug_comments WHERE bug_id = ?", (bug_id,)) + conn.execute("DELETE FROM bug_reports WHERE id = ?", (bug_id,)) + conn.commit() + conn.close() + return {"ok": True} + + +def add_bug_comment(bug_id: int, body: dict, user=Depends(get_current_user)): + """Add a manual comment/note to a bug's log.""" + content = body.get("content", "").strip() + if not content: + raise HTTPException(400, "请输入内容") + conn = get_db() + bug = conn.execute("SELECT id FROM bug_reports WHERE id = ?", (bug_id,)).fetchone() + if not bug: + conn.close() + raise HTTPException(404, "Bug not found") + conn.execute( + "INSERT INTO bug_comments (bug_id, user_id, action, content) VALUES (?, ?, ?, ?)", + (bug_id, user.get("id"), "备注", content), + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/symptom-search") +def symptom_search(body: dict, user=Depends(get_current_user)): + """Search recipes by symptom, notify editors if no good match.""" + query = body.get("query", "").strip() + if not query: + return {"recipes": [], "exact": False} + conn = get_db() + # Search in recipe names + rows = conn.execute( + "SELECT id, name, note, owner_id, version FROM recipes ORDER BY id" + ).fetchall() + exact = [] + related = [] + for r in rows: + if query in r["name"]: + exact.append(_recipe_to_dict(conn, r)) + elif any(c in r["name"] for c in query): + related.append(_recipe_to_dict(conn, r)) + + # If user reports no match, notify editors + if body.get("report_missing"): + who = user.get("display_name") or user.get("username") or "用户" + for role in ("admin", "senior_editor", "editor"): + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + (role, "🔍 用户需求:" + query, + f"{who} 搜索了「{query}」,没有找到满意的配方,请考虑添加。") + ) + conn.execute("INSERT INTO search_log (user_id, query, matched_count) VALUES (?, ?, ?)", + (user.get("id"), query, 0)) + conn.commit() + + conn.close() + return {"exact": exact, "related": related[:20]} + + +# ── Register ──────────────────────────────────────────── +@app.post("/api/register", status_code=201) +def register(body: dict): + import secrets + username = body.get("username", "").strip() + password = body.get("password", "").strip() + display_name = body.get("display_name", "").strip() + if not username or len(username) < 2: + raise HTTPException(400, "用户名至少2个字符") + if not password or len(password) < 4: + raise HTTPException(400, "密码至少4位") + token = secrets.token_hex(24) + conn = get_db() + try: + conn.execute( + "INSERT INTO users (username, token, role, display_name, password) VALUES (?, ?, ?, ?, ?)", + (username, token, "viewer", display_name or username, password) + ) + conn.commit() + except Exception: + conn.close() + raise HTTPException(400, "用户名已被占用") + conn.close() + return {"token": token} + + +# ── Login ─────────────────────────────────────────────── +@app.post("/api/login") +def login(body: dict): + username = body.get("username", "").strip() + password = body.get("password", "").strip() + if not username or not password: + raise HTTPException(400, "请输入用户名和密码") + conn = get_db() + user = conn.execute("SELECT token, password, display_name, role FROM users WHERE username = ?", (username,)).fetchone() + conn.close() + if not user: + raise HTTPException(401, "用户名不存在") + if not user["password"]: + raise HTTPException(401, "该账号未设置密码,请使用链接登录后设置密码") + if user["password"] != password: + raise HTTPException(401, "密码错误") + return {"token": user["token"], "display_name": user["display_name"], "role": user["role"]} + + +@app.put("/api/me") +def update_me(body: dict, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + conn = get_db() + # Update display_name + if "display_name" in body: + dn = body["display_name"].strip() + if not dn: + conn.close() + raise HTTPException(400, "昵称不能为空") + conn.execute("UPDATE users SET display_name = ? WHERE id = ?", (dn, user["id"])) + # Update username + if "username" in body: + un = body["username"].strip() + if not un or len(un) < 2: + conn.close() + raise HTTPException(400, "用户名至少2个字符") + existing = conn.execute("SELECT id FROM users WHERE username = ? AND id != ?", (un, user["id"])).fetchone() + if existing: + conn.close() + raise HTTPException(400, "用户名已被占用") + conn.execute("UPDATE users SET username = ? WHERE id = ?", (un, user["id"])) + # Update password (requires old password verification) + if "password" in body: + pw = body["password"].strip() + if pw and len(pw) < 4: + conn.close() + raise HTTPException(400, "新密码至少4位") + old_pw = body.get("old_password", "").strip() + current_pw = user.get("password") or "" + if current_pw and old_pw != current_pw: + conn.close() + raise HTTPException(400, "当前密码不正确") + if pw: + conn.execute("UPDATE users SET password = ? WHERE id = ?", (pw, user["id"])) + conn.commit() + conn.close() + return {"ok": True} + + +@app.put("/api/me/password") +def set_password(body: dict, user=Depends(get_current_user)): + """Legacy endpoint, kept for compatibility.""" + if not user["id"]: + raise HTTPException(403, "请先登录") + pw = body.get("password", "").strip() + if not pw or len(pw) < 4: + raise HTTPException(400, "密码至少4位") + conn = get_db() + conn.execute("UPDATE users SET password = ? WHERE id = ?", (pw, user["id"])) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Business Verification ────────────────────────────── +@app.post("/api/business-apply", status_code=201) +def business_apply(body: dict, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + conn = get_db() + # Check if already has pending application + existing = conn.execute( + "SELECT id, status FROM business_applications WHERE user_id = ? ORDER BY id DESC LIMIT 1", + (user["id"],) + ).fetchone() + if existing and existing["status"] == "pending": + conn.close() + raise HTTPException(400, "已有待审核的申请") + if user.get("business_verified"): + conn.close() + raise HTTPException(400, "已是认证商业用户") + business_name = body.get("business_name", "").strip() + document = body.get("document", "") # base64 image + if not business_name: + conn.close() + raise HTTPException(400, "请填写商户名称") + if document and len(document) > 2000000: + conn.close() + raise HTTPException(400, "文件太大,请压缩到1.5MB以内") + conn.execute( + "INSERT INTO business_applications (user_id, business_name, document) VALUES (?, ?, ?)", + (user["id"], business_name, document) + ) + who = user.get("display_name") or user.get("username") + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + ("admin", "🏢 商业认证申请", f"{who} 申请商业用户认证,商户名:{business_name}") + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.get("/api/my-business-application") +def get_my_business_application(user=Depends(get_current_user)): + if not user["id"]: + return {"status": None} + conn = get_db() + row = conn.execute( + "SELECT business_name, document, status, reject_reason, created_at FROM business_applications WHERE user_id = ? ORDER BY id DESC LIMIT 1", + (user["id"],) + ).fetchone() + conn.close() + if not row: + return {"status": None} + return dict(row) + + +@app.get("/api/business-applications") +def list_business_applications(user=Depends(require_role("admin"))): + conn = get_db() + rows = conn.execute( + "SELECT a.id, a.user_id, a.business_name, a.document, a.status, a.created_at, " + "u.display_name, u.username FROM business_applications a " + "LEFT JOIN users u ON a.user_id = u.id ORDER BY a.id DESC" + ).fetchall() + conn.close() + return [dict(r) for r in rows] + + +@app.post("/api/business-applications/{app_id}/approve") +def approve_business(app_id: int, user=Depends(require_role("admin"))): + conn = get_db() + app = conn.execute("SELECT user_id, business_name FROM business_applications WHERE id = ?", (app_id,)).fetchone() + if not app: + conn.close() + raise HTTPException(404, "申请不存在") + conn.execute("UPDATE business_applications SET status = 'approved', reviewed_at = datetime('now') WHERE id = ?", (app_id,)) + conn.execute("UPDATE users SET business_verified = 1 WHERE id = ?", (app["user_id"],)) + # Notify user + target = conn.execute("SELECT role FROM users WHERE id = ?", (app["user_id"],)).fetchone() + if target: + conn.execute( + "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)", + (target["role"], "🎉 商业认证通过", "恭喜!你的商业用户认证已通过,现在可以使用项目核算等商业功能。", app["user_id"]) + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/business-applications/{app_id}/reject") +def reject_business(app_id: int, body: dict = None, user=Depends(require_role("admin"))): + conn = get_db() + app = conn.execute("SELECT user_id FROM business_applications WHERE id = ?", (app_id,)).fetchone() + if not app: + conn.close() + raise HTTPException(404, "申请不存在") + reason = (body or {}).get("reason", "").strip() + conn.execute("UPDATE business_applications SET status = 'rejected', reviewed_at = datetime('now'), reject_reason = ? WHERE id = ?", (reason, app_id)) + target = conn.execute("SELECT role FROM users WHERE id = ?", (app["user_id"],)).fetchone() + if target: + msg = "你的商业用户认证申请未通过。" + if reason: + msg += "\n\n原因:" + reason + msg += "\n\n你可以修改后重新申请。" + conn.execute( + "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)", + (target["role"], "商业认证未通过", msg, app["user_id"]) + ) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Translation Suggestions ──────────────────────────── +@app.post("/api/translation-suggest", status_code=201) +def suggest_translation(body: dict, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + recipe_name = body.get("recipe_name", "").strip() + suggested_en = body.get("suggested_en", "").strip() + recipe_id = body.get("recipe_id") + if not recipe_name or not suggested_en: + raise HTTPException(400, "请填写翻译") + conn = get_db() + conn.execute( + "INSERT INTO translation_suggestions (recipe_id, recipe_name, suggested_en, user_id) VALUES (?, ?, ?, ?)", + (recipe_id, recipe_name, suggested_en, user["id"]) + ) + who = user.get("display_name") or user.get("username") + if user["role"] != "admin": + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + ("admin", "📝 翻译建议", f"{who} 建议将「{recipe_name}」翻译为「{suggested_en}」") + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.get("/api/translation-suggestions") +def list_translation_suggestions(user=Depends(require_role("admin"))): + conn = get_db() + rows = conn.execute( + "SELECT s.id, s.recipe_id, s.recipe_name, s.suggested_en, s.status, s.created_at, " + "u.display_name, u.username FROM translation_suggestions s " + "LEFT JOIN users u ON s.user_id = u.id WHERE s.status = 'pending' ORDER BY s.id DESC" + ).fetchall() + conn.close() + return [dict(r) for r in rows] + + +@app.post("/api/translation-suggestions/{sid}/approve") +def approve_translation(sid: int, user=Depends(require_role("admin"))): + conn = get_db() + row = conn.execute("SELECT recipe_name, suggested_en FROM translation_suggestions WHERE id = ?", (sid,)).fetchone() + if not row: + conn.close() + raise HTTPException(404) + conn.execute("UPDATE translation_suggestions SET status = 'approved' WHERE id = ?", (sid,)) + conn.commit() + conn.close() + return {"ok": True, "recipe_name": row["recipe_name"], "suggested_en": row["suggested_en"]} + + +@app.post("/api/translation-suggestions/{sid}/reject") +def reject_translation(sid: int, user=Depends(require_role("admin"))): + conn = get_db() + conn.execute("UPDATE translation_suggestions SET status = 'rejected' WHERE id = ?", (sid,)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/business-revoke/{user_id}") +def revoke_business(user_id: int, body: dict = None, user=Depends(require_role("admin"))): + conn = get_db() + conn.execute("UPDATE users SET business_verified = 0 WHERE id = ?", (user_id,)) + reason = (body or {}).get("reason", "").strip() + target = conn.execute("SELECT role FROM users WHERE id = ?", (user_id,)).fetchone() + if target: + msg = "你的商业用户资格已被取消。" + if reason: + msg += "\n\n原因:" + reason + msg += "\n\n如有疑问请联系管理员,也可重新申请认证。" + conn.execute( + "INSERT INTO notifications (target_role, title, body, target_user_id) VALUES (?, ?, ?, ?)", + (target["role"], "商业资格已取消", msg, user_id) + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/impersonate") +def impersonate(body: dict, user=Depends(require_role("admin"))): + target_id = body.get("user_id") + if not target_id: + raise HTTPException(400, "user_id required") + conn = get_db() + target = conn.execute("SELECT token FROM users WHERE id = ?", (target_id,)).fetchone() + conn.close() + if not target: + raise HTTPException(404, "User not found") + return {"token": target["token"]} + + +# ── Oils ──────────────────────────────────────────────── +@app.get("/api/oils") +def list_oils(): + conn = get_db() + rows = conn.execute("SELECT name, bottle_price, drop_count, retail_price, is_active FROM oils ORDER BY name").fetchall() + conn.close() + return [dict(r) for r in rows] + + +@app.post("/api/oils", status_code=201) +def upsert_oil(oil: OilIn, user=Depends(require_role("admin", "senior_editor"))): + conn = get_db() + conn.execute( + "INSERT INTO oils (name, bottle_price, drop_count, retail_price) VALUES (?, ?, ?, ?) " + "ON CONFLICT(name) DO UPDATE SET bottle_price=excluded.bottle_price, drop_count=excluded.drop_count, retail_price=excluded.retail_price", + (oil.name, oil.bottle_price, oil.drop_count, oil.retail_price), + ) + log_audit(conn, user["id"], "upsert_oil", "oil", oil.name, oil.name, + json.dumps({"bottle_price": oil.bottle_price, "drop_count": oil.drop_count})) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/oils/{name}") +def delete_oil(name: str, user=Depends(require_role("admin", "senior_editor"))): + conn = get_db() + row = conn.execute("SELECT name, bottle_price, drop_count, retail_price, is_active FROM oils WHERE name = ?", (name,)).fetchone() + snapshot = dict(row) if row else {} + conn.execute("DELETE FROM oils WHERE name = ?", (name,)) + log_audit(conn, user["id"], "delete_oil", "oil", name, name, + json.dumps(snapshot, ensure_ascii=False)) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Recipes ───────────────────────────────────────────── +def _recipe_to_dict(conn, row): + rid = row["id"] + ings = conn.execute( + "SELECT oil_name, drops FROM recipe_ingredients WHERE recipe_id = ?", (rid,) + ).fetchall() + tags = conn.execute( + "SELECT tag_name FROM recipe_tags WHERE recipe_id = ?", (rid,) + ).fetchall() + owner = conn.execute( + "SELECT display_name, username FROM users WHERE id = ?", (row["owner_id"],) + ).fetchone() if row["owner_id"] else None + return { + "id": rid, + "name": row["name"], + "note": row["note"], + "owner_id": row["owner_id"], + "owner_name": (owner["display_name"] or owner["username"]) if owner else None, + "version": row["version"] if "version" in row.keys() else 1, + "ingredients": [{"oil_name": i["oil_name"], "drops": i["drops"]} for i in ings], + "tags": [t["tag_name"] for t in tags], + } + + +@app.get("/api/recipes") +def list_recipes(user=Depends(get_current_user)): + conn = get_db() + # Admin sees all; others see admin-owned (adopted) + their own + if user["role"] == "admin": + rows = conn.execute("SELECT id, name, note, owner_id, version FROM recipes ORDER BY id").fetchall() + else: + admin = conn.execute("SELECT id FROM users WHERE role = 'admin' LIMIT 1").fetchone() + admin_id = admin["id"] if admin else 1 + user_id = user.get("id") + if user_id: + rows = conn.execute( + "SELECT id, name, note, owner_id, version FROM recipes WHERE owner_id = ? OR owner_id = ? ORDER BY id", + (admin_id, user_id) + ).fetchall() + else: + rows = conn.execute( + "SELECT id, name, note, owner_id, version FROM recipes WHERE owner_id = ? ORDER BY id", + (admin_id,) + ).fetchall() + result = [_recipe_to_dict(conn, r) for r in rows] + conn.close() + return result + + +@app.get("/api/recipes/{recipe_id}") +def get_recipe(recipe_id: int): + conn = get_db() + row = conn.execute("SELECT id, name, note, owner_id, version FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + if not row: + conn.close() + raise HTTPException(404, "Recipe not found") + result = _recipe_to_dict(conn, row) + conn.close() + return result + + +@app.post("/api/recipes", status_code=201) +def create_recipe(recipe: RecipeIn, user=Depends(require_role("admin", "senior_editor", "editor"))): + conn = get_db() + c = conn.cursor() + c.execute("INSERT INTO recipes (name, note, owner_id) VALUES (?, ?, ?)", + (recipe.name, recipe.note, user["id"])) + rid = c.lastrowid + for ing in recipe.ingredients: + c.execute( + "INSERT INTO recipe_ingredients (recipe_id, oil_name, drops) VALUES (?, ?, ?)", + (rid, ing.oil_name, ing.drops), + ) + for tag in recipe.tags: + c.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,)) + c.execute("INSERT OR IGNORE INTO recipe_tags (recipe_id, tag_name) VALUES (?, ?)", (rid, tag)) + log_audit(conn, user["id"], "create_recipe", "recipe", rid, recipe.name) + # Notify admin when non-admin creates a recipe + if user["role"] != "admin": + who = user.get("display_name") or user["username"] + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + ("admin", "📝 新配方待审核", + f"{who} 新增了配方「{recipe.name}」,请到管理配方查看并采纳。") + ) + conn.commit() + conn.close() + return {"id": rid} + + +def _check_recipe_permission(conn, recipe_id, user): + """Check if user can modify this recipe.""" + row = conn.execute("SELECT owner_id, name FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + if not row: + raise HTTPException(404, "Recipe not found") + if user["role"] in ("admin", "senior_editor"): + return row + if user["role"] == "editor" and row["owner_id"] == user["id"]: + return row + raise HTTPException(403, "只能修改自己创建的配方") + + +@app.put("/api/recipes/{recipe_id}") +def update_recipe(recipe_id: int, update: RecipeUpdate, user=Depends(require_role("admin", "senior_editor", "editor"))): + conn = get_db() + c = conn.cursor() + _check_recipe_permission(conn, recipe_id, user) + + # Optimistic locking: check version if provided + if update.version is not None: + current = c.execute("SELECT version FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + if current and current["version"] and current["version"] != update.version: + conn.close() + raise HTTPException(409, "此配方已被其他人修改,请刷新后重试") + + if update.name is not None: + c.execute("UPDATE recipes SET name = ? WHERE id = ?", (update.name, recipe_id)) + if update.note is not None: + c.execute("UPDATE recipes SET note = ? WHERE id = ?", (update.note, recipe_id)) + if update.ingredients is not None: + c.execute("DELETE FROM recipe_ingredients WHERE recipe_id = ?", (recipe_id,)) + for ing in update.ingredients: + c.execute( + "INSERT INTO recipe_ingredients (recipe_id, oil_name, drops) VALUES (?, ?, ?)", + (recipe_id, ing.oil_name, ing.drops), + ) + if update.tags is not None: + c.execute("DELETE FROM recipe_tags WHERE recipe_id = ?", (recipe_id,)) + for tag in update.tags: + c.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,)) + c.execute( + "INSERT OR IGNORE INTO recipe_tags (recipe_id, tag_name) VALUES (?, ?)", + (recipe_id, tag), + ) + c.execute("UPDATE recipes SET updated_by = ?, version = COALESCE(version, 1) + 1 WHERE id = ?", (user["id"], recipe_id)) + log_audit(conn, user["id"], "update_recipe", "recipe", recipe_id, update.name) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/recipes/{recipe_id}") +def delete_recipe(recipe_id: int, user=Depends(require_role("admin", "senior_editor", "editor"))): + conn = get_db() + row = _check_recipe_permission(conn, recipe_id, user) + # Save full snapshot for undo + full = conn.execute("SELECT id, name, note, owner_id, version FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + snapshot = _recipe_to_dict(conn, full) + log_audit(conn, user["id"], "delete_recipe", "recipe", recipe_id, row["name"], + json.dumps(snapshot, ensure_ascii=False)) + conn.execute("DELETE FROM recipes WHERE id = ?", (recipe_id,)) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Adopt (admin takes ownership of editor recipes) ──── +@app.post("/api/recipes/{recipe_id}/adopt") +def adopt_recipe(recipe_id: int, user=Depends(require_role("admin"))): + conn = get_db() + row = conn.execute("SELECT id, name, owner_id FROM recipes WHERE id = ?", (recipe_id,)).fetchone() + if not row: + conn.close() + raise HTTPException(404, "Recipe not found") + if row["owner_id"] == user["id"]: + conn.close() + return {"ok": True, "msg": "already owned"} + old_owner = conn.execute("SELECT display_name, username FROM users WHERE id = ?", (row["owner_id"],)).fetchone() + old_name = (old_owner["display_name"] or old_owner["username"]) if old_owner else "unknown" + conn.execute("UPDATE recipes SET owner_id = ?, updated_by = ? WHERE id = ?", (user["id"], user["id"], recipe_id)) + log_audit(conn, user["id"], "adopt_recipe", "recipe", recipe_id, row["name"], + json.dumps({"from_user": old_name})) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/recipes/adopt-batch") +def adopt_batch(body: dict, user=Depends(require_role("admin"))): + ids = body.get("ids", []) + if not ids: + raise HTTPException(400, "No recipe ids provided") + conn = get_db() + adopted = 0 + for rid in ids: + row = conn.execute("SELECT id, name, owner_id FROM recipes WHERE id = ?", (rid,)).fetchone() + if row and row["owner_id"] != user["id"]: + conn.execute("UPDATE recipes SET owner_id = ?, updated_by = ? WHERE id = ?", (user["id"], user["id"], rid)) + log_audit(conn, user["id"], "adopt_recipe", "recipe", rid, row["name"]) + adopted += 1 + conn.commit() + conn.close() + return {"ok": True, "adopted": adopted} + + +# ── Tags ──────────────────────────────────────────────── +@app.get("/api/tags") +def list_tags(): + conn = get_db() + rows = conn.execute("SELECT name FROM tags ORDER BY name").fetchall() + conn.close() + return [r["name"] for r in rows] + + +@app.post("/api/tags", status_code=201) +def create_tag(body: dict, user=Depends(require_role("admin", "senior_editor", "editor"))): + name = body.get("name", "").strip() + if not name: + raise HTTPException(400, "Tag name required") + conn = get_db() + conn.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (name,)) + # Don't log tag creation (too frequent/noisy) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/tags/{name}") +def delete_tag(name: str, user=Depends(require_role("admin"))): + conn = get_db() + conn.execute("DELETE FROM recipe_tags WHERE tag_name = ?", (name,)) + conn.execute("DELETE FROM tags WHERE name = ?", (name,)) + log_audit(conn, user["id"], "delete_tag", "tag", name, name) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Users (admin only) ───────────────────────────────── +@app.get("/api/users") +def list_users(user=Depends(require_role("admin"))): + conn = get_db() + rows = conn.execute("SELECT id, username, token, role, display_name, created_at, business_verified FROM users ORDER BY id").fetchall() + conn.close() + return [dict(r) for r in rows] + + +@app.post("/api/users", status_code=201) +def create_user(body: UserIn, user=Depends(require_role("admin"))): + import secrets + token = secrets.token_hex(24) + conn = get_db() + try: + conn.execute( + "INSERT INTO users (username, token, role, display_name) VALUES (?, ?, ?, ?)", + (body.username, token, body.role, body.display_name), + ) + log_audit(conn, user["id"], "create_user", "user", body.username, body.display_name, + json.dumps({"role": body.role})) + conn.commit() + except Exception: + conn.close() + raise HTTPException(400, "用户名已存在") + conn.close() + return {"token": token, "username": body.username} + + +@app.delete("/api/users/{user_id}") +def delete_user(user_id: int, user=Depends(require_role("admin"))): + if user_id == user["id"]: + raise HTTPException(400, "不能删除自己") + conn = get_db() + target = conn.execute("SELECT id, username, token, role, display_name FROM users WHERE id = ?", (user_id,)).fetchone() + if not target: + conn.close() + raise HTTPException(404, "User not found") + snapshot = dict(target) + conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) + log_audit(conn, user["id"], "delete_user", "user", user_id, target["username"], + json.dumps(snapshot, ensure_ascii=False)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.put("/api/users/{user_id}") +def update_user(user_id: int, body: UserUpdate, user=Depends(require_role("admin"))): + conn = get_db() + if body.role is not None: + conn.execute("UPDATE users SET role = ? WHERE id = ?", (body.role, user_id)) + if body.display_name is not None: + conn.execute("UPDATE users SET display_name = ? WHERE id = ?", (body.display_name, user_id)) + log_audit(conn, user["id"], "update_user", "user", user_id, None, + json.dumps({"role": body.role, "display_name": body.display_name})) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Undo (admin only) ────────────────────────────────── +@app.post("/api/audit-log/{log_id}/undo") +def undo_action(log_id: int, user=Depends(require_role("admin"))): + conn = get_db() + entry = conn.execute("SELECT * FROM audit_log WHERE id = ?", (log_id,)).fetchone() + if not entry: + conn.close() + raise HTTPException(404, "Audit log entry not found") + + action = entry["action"] + detail = entry["detail"] + if not detail: + conn.close() + raise HTTPException(400, "此操作无法撤销(无快照数据)") + + snapshot = json.loads(detail) + + if action == "delete_recipe": + # Restore recipe from snapshot + c = conn.cursor() + c.execute("INSERT INTO recipes (name, note, owner_id) VALUES (?, ?, ?)", + (snapshot["name"], snapshot.get("note", ""), snapshot.get("owner_id"))) + new_id = c.lastrowid + for ing in snapshot.get("ingredients", []): + c.execute("INSERT INTO recipe_ingredients (recipe_id, oil_name, drops) VALUES (?, ?, ?)", + (new_id, ing["oil_name"], ing["drops"])) + for tag in snapshot.get("tags", []): + c.execute("INSERT OR IGNORE INTO tags (name) VALUES (?)", (tag,)) + c.execute("INSERT OR IGNORE INTO recipe_tags (recipe_id, tag_name) VALUES (?, ?)", (new_id, tag)) + log_audit(conn, user["id"], "undo_delete_recipe", "recipe", new_id, snapshot["name"], + json.dumps({"original_log_id": log_id})) + conn.commit() + conn.close() + return {"ok": True, "new_id": new_id} + + elif action == "delete_oil": + conn.execute( + "INSERT OR IGNORE INTO oils (name, bottle_price, drop_count) VALUES (?, ?, ?)", + (snapshot["name"], snapshot["bottle_price"], snapshot["drop_count"])) + log_audit(conn, user["id"], "undo_delete_oil", "oil", snapshot["name"], snapshot["name"], + json.dumps({"original_log_id": log_id})) + conn.commit() + conn.close() + return {"ok": True} + + elif action == "delete_user": + try: + conn.execute( + "INSERT INTO users (username, token, role, display_name) VALUES (?, ?, ?, ?)", + (snapshot["username"], snapshot["token"], snapshot["role"], snapshot.get("display_name", ""))) + log_audit(conn, user["id"], "undo_delete_user", "user", snapshot["username"], snapshot.get("display_name"), + json.dumps({"original_log_id": log_id})) + conn.commit() + except Exception: + conn.close() + raise HTTPException(400, "恢复失败(用户名可能已被占用)") + conn.close() + return {"ok": True} + + conn.close() + raise HTTPException(400, "此操作类型不支持撤销") + + +# ── Audit Log (admin only) ───────────────────────────── +@app.get("/api/audit-log") +def get_audit_log(limit: int = 100, offset: int = 0, user=Depends(require_role("admin"))): + conn = get_db() + rows = conn.execute( + "SELECT a.id, a.action, a.target_type, a.target_id, a.target_name, a.detail, a.created_at, " + "u.display_name as user_name, u.username " + "FROM audit_log a LEFT JOIN users u ON a.user_id = u.id " + "ORDER BY a.id DESC LIMIT ? OFFSET ?", + (limit, offset), + ).fetchall() + conn.close() + return [dict(r) for r in rows] + + +# ── Brand Settings ───────────────────────────────────── +@app.get("/api/brand") +def get_brand(user=Depends(get_current_user)): + if not user["id"]: + return {"qr_code": None, "brand_logo": None, "brand_bg": None, "brand_name": None, "brand_align": "center"} + conn = get_db() + row = conn.execute("SELECT qr_code, brand_logo, brand_bg, brand_name, brand_align FROM users WHERE id = ?", (user["id"],)).fetchone() + conn.close() + if not row: + return {"qr_code": None, "brand_logo": None, "brand_bg": None, "brand_name": None, "brand_align": "center"} + return dict(row) + + +@app.put("/api/brand") +def update_brand(body: dict, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + conn = get_db() + if "qr_code" in body: + # Limit size: ~500KB base64 + if body["qr_code"] and len(body["qr_code"]) > 700000: + raise HTTPException(400, "图片太大,请压缩后上传") + conn.execute("UPDATE users SET qr_code = ? WHERE id = ?", (body["qr_code"], user["id"])) + if "brand_logo" in body: + if body["brand_logo"] and len(body["brand_logo"]) > 700000: + raise HTTPException(400, "图片太大,请压缩后上传") + conn.execute("UPDATE users SET brand_logo = ? WHERE id = ?", (body["brand_logo"], user["id"])) + if "brand_bg" in body: + if body["brand_bg"] and len(body["brand_bg"]) > 1500000: + raise HTTPException(400, "背景图太大,请压缩到1MB以内") + conn.execute("UPDATE users SET brand_bg = ? WHERE id = ?", (body["brand_bg"], user["id"])) + if "brand_name" in body: + conn.execute("UPDATE users SET brand_name = ? WHERE id = ?", (body["brand_name"], user["id"])) + if "brand_align" in body: + conn.execute("UPDATE users SET brand_align = ? WHERE id = ?", (body["brand_align"], user["id"])) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Profit Projects ──────────────────────────────────── +@app.get("/api/projects") +def list_projects(): + conn = get_db() + rows = conn.execute("SELECT * FROM profit_projects ORDER BY id DESC").fetchall() + conn.close() + return [{ **dict(r), "ingredients": json.loads(r["ingredients"]) } for r in rows] + + +@app.post("/api/projects", status_code=201) +def create_project(body: dict, user=Depends(require_role("admin", "senior_editor"))): + conn = get_db() + c = conn.cursor() + c.execute( + "INSERT INTO profit_projects (name, ingredients, pricing, note, created_by) VALUES (?, ?, ?, ?, ?)", + (body["name"], json.dumps(body.get("ingredients", []), ensure_ascii=False), + body.get("pricing", 0), body.get("note", ""), user["id"]) + ) + conn.commit() + pid = c.lastrowid + conn.close() + return {"id": pid} + + +@app.put("/api/projects/{pid}") +def update_project(pid: int, body: dict, user=Depends(require_role("admin", "senior_editor"))): + conn = get_db() + if "name" in body: + conn.execute("UPDATE profit_projects SET name = ? WHERE id = ?", (body["name"], pid)) + if "ingredients" in body: + conn.execute("UPDATE profit_projects SET ingredients = ? WHERE id = ?", + (json.dumps(body["ingredients"], ensure_ascii=False), pid)) + if "pricing" in body: + conn.execute("UPDATE profit_projects SET pricing = ? WHERE id = ?", (body["pricing"], pid)) + if "note" in body: + conn.execute("UPDATE profit_projects SET note = ? WHERE id = ?", (body["note"], pid)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/projects/{pid}") +def delete_project(pid: int, user=Depends(require_role("admin", "senior_editor"))): + conn = get_db() + conn.execute("DELETE FROM profit_projects WHERE id = ?", (pid,)) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Diary (personal recipes + journal) ───────────────── +@app.get("/api/diary") +def list_diary(user=Depends(get_current_user)): + if not user["id"]: + return [] + conn = get_db() + rows = conn.execute( + "SELECT id, source_recipe_id, name, ingredients, note, tags, created_at " + "FROM user_diary WHERE user_id = ? ORDER BY id DESC", (user["id"],) + ).fetchall() + result = [] + for r in rows: + entries = conn.execute( + "SELECT id, content, created_at FROM diary_entries WHERE diary_id = ? ORDER BY created_at DESC", (r["id"],) + ).fetchall() + d = dict(r) + d["ingredients"] = json.loads(r["ingredients"]) + d["tags"] = json.loads(r["tags"] or "[]") + d["entries"] = [dict(e) for e in entries] + result.append(d) + conn.close() + return result + + +@app.post("/api/diary", status_code=201) +def create_diary(body: dict, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + name = body.get("name", "").strip() + ingredients = body.get("ingredients", []) + note = body.get("note", "") + source_id = body.get("source_recipe_id") + if not name: + raise HTTPException(400, "请输入配方名称") + conn = get_db() + c = conn.cursor() + c.execute( + "INSERT INTO user_diary (user_id, source_recipe_id, name, ingredients, note) VALUES (?, ?, ?, ?, ?)", + (user["id"], source_id, name, json.dumps(ingredients, ensure_ascii=False), note) + ) + conn.commit() + did = c.lastrowid + conn.close() + return {"id": did} + + +@app.put("/api/diary/{diary_id}") +def update_diary(diary_id: int, body: dict, user=Depends(get_current_user)): + conn = get_db() + row = conn.execute("SELECT user_id FROM user_diary WHERE id = ?", (diary_id,)).fetchone() + if not row or row["user_id"] != user["id"]: + conn.close() + raise HTTPException(403, "无权操作") + if "name" in body: + conn.execute("UPDATE user_diary SET name = ? WHERE id = ?", (body["name"], diary_id)) + if "note" in body: + conn.execute("UPDATE user_diary SET note = ? WHERE id = ?", (body["note"], diary_id)) + if "ingredients" in body: + conn.execute("UPDATE user_diary SET ingredients = ? WHERE id = ?", + (json.dumps(body["ingredients"], ensure_ascii=False), diary_id)) + if "tags" in body: + conn.execute("UPDATE user_diary SET tags = ? WHERE id = ?", + (json.dumps(body["tags"], ensure_ascii=False), diary_id)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/diary/{diary_id}") +def delete_diary(diary_id: int, user=Depends(get_current_user)): + conn = get_db() + row = conn.execute("SELECT user_id FROM user_diary WHERE id = ?", (diary_id,)).fetchone() + if not row or row["user_id"] != user["id"]: + conn.close() + raise HTTPException(403, "无权操作") + conn.execute("DELETE FROM user_diary WHERE id = ?", (diary_id,)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/diary/{diary_id}/entries", status_code=201) +def add_diary_entry(diary_id: int, body: dict, user=Depends(get_current_user)): + conn = get_db() + row = conn.execute("SELECT user_id FROM user_diary WHERE id = ?", (diary_id,)).fetchone() + if not row or row["user_id"] != user["id"]: + conn.close() + raise HTTPException(403, "无权操作") + content = body.get("content", "").strip() + if not content: + raise HTTPException(400, "内容不能为空") + conn.execute("INSERT INTO diary_entries (diary_id, content) VALUES (?, ?)", (diary_id, content)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/diary/entries/{entry_id}") +def delete_diary_entry(entry_id: int, user=Depends(get_current_user)): + conn = get_db() + row = conn.execute( + "SELECT d.user_id FROM diary_entries e JOIN user_diary d ON e.diary_id = d.id WHERE e.id = ?", + (entry_id,) + ).fetchone() + if not row or row["user_id"] != user["id"]: + conn.close() + raise HTTPException(403, "无权操作") + conn.execute("DELETE FROM diary_entries WHERE id = ?", (entry_id,)) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Favorites ────────────────────────────────────────── +@app.get("/api/favorites") +def get_favorites(user=Depends(get_current_user)): + if not user["id"]: + return [] + conn = get_db() + rows = conn.execute("SELECT recipe_id FROM user_favorites WHERE user_id = ? ORDER BY created_at DESC", (user["id"],)).fetchall() + conn.close() + return [r["recipe_id"] for r in rows] + + +@app.post("/api/favorites/{recipe_id}", status_code=201) +def add_favorite(recipe_id: int, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + conn = get_db() + conn.execute("INSERT OR IGNORE INTO user_favorites (user_id, recipe_id) VALUES (?, ?)", (user["id"], recipe_id)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/favorites/{recipe_id}") +def remove_favorite(recipe_id: int, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + conn = get_db() + conn.execute("DELETE FROM user_favorites WHERE user_id = ? AND recipe_id = ?", (user["id"], recipe_id)) + conn.commit() + conn.close() + return {"ok": True} + + +# ── User Inventory ───────────────────────────────────── +@app.get("/api/inventory") +def get_inventory(user=Depends(get_current_user)): + if not user["id"]: + return [] + conn = get_db() + rows = conn.execute("SELECT oil_name FROM user_inventory WHERE user_id = ? ORDER BY oil_name", (user["id"],)).fetchall() + conn.close() + return [r["oil_name"] for r in rows] + + +@app.post("/api/inventory", status_code=201) +def add_inventory(body: dict, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + name = body.get("oil_name", "").strip() + if not name: + raise HTTPException(400, "oil_name required") + conn = get_db() + conn.execute("INSERT OR IGNORE INTO user_inventory (user_id, oil_name) VALUES (?, ?)", (user["id"], name)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/inventory/{oil_name}") +def remove_inventory(oil_name: str, user=Depends(get_current_user)): + if not user["id"]: + raise HTTPException(403, "请先登录") + conn = get_db() + conn.execute("DELETE FROM user_inventory WHERE user_id = ? AND oil_name = ?", (user["id"], oil_name)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.get("/api/inventory/recipes") +def recipes_by_inventory(user=Depends(get_current_user)): + """Get recipes that can be made with user's inventory oils.""" + if not user["id"]: + return [] + conn = get_db() + inv = [r["oil_name"] for r in conn.execute( + "SELECT oil_name FROM user_inventory WHERE user_id = ?", (user["id"],)).fetchall()] + if not inv: + conn.close() + return [] + rows = conn.execute("SELECT id, name, note, owner_id, version FROM recipes ORDER BY id").fetchall() + result = [] + for r in rows: + recipe = _recipe_to_dict(conn, r) + eo_oils = [i["oil_name"] for i in recipe["ingredients"] if i["oil_name"] != "椰子油"] + matched = [o for o in eo_oils if o in inv] + if matched: + recipe["inventory_match"] = len(matched) + recipe["inventory_total"] = len(eo_oils) + recipe["inventory_missing"] = [o for o in eo_oils if o not in inv] + result.append(recipe) + conn.close() + result.sort(key=lambda r: (-r["inventory_match"], r["inventory_total"] - r["inventory_match"])) + return result + + +# ── Search Logging ───────────────────────────────────── +@app.post("/api/search-log") +def log_search(body: dict, user=Depends(get_current_user)): + query = body.get("query", "").strip() + matched = body.get("matched_count", 0) + if not query: + return {"ok": True} + conn = get_db() + conn.execute("INSERT INTO search_log (user_id, query, matched_count) VALUES (?, ?, ?)", + (user.get("id"), query, matched)) + # Instant notification when no match found + if matched == 0: + who = user.get("display_name") or user.get("username") or "用户" + for role in ("admin", "senior_editor"): + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + (role, "🔍 有人搜索了未收录的配方", + f"{who} 搜索了「{query}」但没有找到匹配配方,请考虑添加。") + ) + conn.commit() + conn.close() + return {"ok": True, "matched": matched} + + +@app.get("/api/search-log/unmatched") +def get_unmatched_searches(days: int = 7, user=Depends(require_role("admin", "senior_editor"))): + conn = get_db() + rows = conn.execute( + "SELECT query, COUNT(*) as cnt, MAX(created_at) as last_at " + "FROM search_log WHERE matched_count = 0 AND created_at > datetime('now', ?) " + "GROUP BY query ORDER BY cnt DESC LIMIT 50", + (f"-{days} days",) + ).fetchall() + conn.close() + return [dict(r) for r in rows] + + +# ── Notifications ────────────────────────────────────── +@app.get("/api/notifications") +def get_notifications(user=Depends(get_current_user)): + if not user["id"]: + return [] + conn = get_db() + rows = conn.execute( + "SELECT id, title, body, is_read, created_at FROM notifications " + "WHERE (target_user_id = ? OR (target_user_id IS NULL AND (target_role = ? OR target_role = 'all'))) " + "ORDER BY is_read ASC, id DESC LIMIT 200", + (user["id"], user["role"]) + ).fetchall() + conn.close() + return [dict(r) for r in rows] + + +@app.post("/api/notifications/{nid}/read") +def mark_notification_read(nid: int, body: dict = None, user=Depends(get_current_user)): + conn = get_db() + notif = conn.execute("SELECT title FROM notifications WHERE id = ?", (nid,)).fetchone() + # Bug test notifications can only be marked read with force=true (from "已测试" flow) + force = (body or {}).get("force", False) if body else False + if notif and "待测试" in (notif["title"] or "") and not force: + conn.close() + raise HTTPException(400, "请先点击「已测试」完成测试") + conn.execute("UPDATE notifications SET is_read = 1 WHERE id = ?", (nid,)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/notifications/{nid}/unread") +def mark_notification_unread(nid: int, user=Depends(get_current_user)): + conn = get_db() + conn.execute("UPDATE notifications SET is_read = 0 WHERE id = ?", (nid,)) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/notifications/read-all") +def mark_all_notifications_read(user=Depends(get_current_user)): + conn = get_db() + # Mark all as read EXCEPT bug test notifications (title contains '待测试') + conn.execute( + "UPDATE notifications SET is_read = 1 WHERE is_read = 0 " + "AND title NOT LIKE '%待测试%' " + "AND (target_user_id = ? OR (target_user_id IS NULL AND (target_role = ? OR target_role = 'all')))", + (user["id"], user["role"]) + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.post("/api/cron/weekly-review") +def weekly_review(user=Depends(require_role("admin"))): + """Generate weekly notifications. Call via cron or manually.""" + conn = get_db() + + # 1. Pending recipes for admin review + admin = conn.execute("SELECT id FROM users WHERE role = 'admin' LIMIT 1").fetchone() + admin_id = admin["id"] if admin else 1 + pending = conn.execute( + "SELECT COUNT(*) as cnt FROM recipes WHERE owner_id != ?", (admin_id,) + ).fetchone()["cnt"] + if pending > 0: + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + ("admin", f"有 {pending} 条配方待审核", + "其他用户新增了配方,请到「管理配方」→「待审核」查看并采纳。") + ) + + # 2. Unmatched searches for admin + senior_editor + unmatched = conn.execute( + "SELECT query, COUNT(*) as cnt FROM search_log " + "WHERE matched_count = 0 AND created_at > datetime('now', '-7 days') " + "GROUP BY query ORDER BY cnt DESC LIMIT 10" + ).fetchall() + if unmatched: + queries = "、".join([f"「{r['query']}」({r['cnt']}次)" for r in unmatched]) + for role in ("admin", "senior_editor"): + conn.execute( + "INSERT INTO notifications (target_role, title, body) VALUES (?, ?, ?)", + (role, "本周未匹配的搜索需求", + f"以下搜索没有找到配方:{queries}。请考虑完善相关配方。") + ) + + conn.commit() + conn.close() + return {"ok": True, "pending_recipes": pending, "unmatched_queries": len(unmatched)} + + +# ── Category Modules (homepage) ──────────────────────── +@app.get("/api/categories") +def list_categories(): + conn = get_db() + rows = conn.execute("SELECT * FROM category_modules ORDER BY sort_order, id").fetchall() + conn.close() + return [dict(r) for r in rows] + + +@app.post("/api/categories", status_code=201) +def create_category(body: dict, user=Depends(require_role("admin"))): + conn = get_db() + conn.execute( + "INSERT INTO category_modules (name, subtitle, icon, bg_image, color_from, color_to, tag_name, sort_order) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (body["name"], body.get("subtitle", ""), body.get("icon", "🌿"), body.get("bg_image", ""), + body.get("color_from", "#7a9e7e"), body.get("color_to", "#5a7d5e"), + body["tag_name"], body.get("sort_order", 0)) + ) + conn.commit() + conn.close() + return {"ok": True} + + +@app.delete("/api/categories/{cat_id}") +def delete_category(cat_id: int, user=Depends(require_role("admin"))): + conn = get_db() + conn.execute("DELETE FROM category_modules WHERE id = ?", (cat_id,)) + conn.commit() + conn.close() + return {"ok": True} + + +# ── Static files (frontend) ──────────────────────────── +FRONTEND_DIR = os.environ.get("FRONTEND_DIR", "/app/frontend") + + +@app.on_event("startup") +def startup(): + init_db() + defaults_path = os.path.join(os.path.dirname(__file__), "defaults.json") + if os.path.exists(defaults_path): + with open(defaults_path) as f: + data = json.load(f) + seed_defaults(data["oils_meta"], data["recipes"]) + + if os.path.isdir(FRONTEND_DIR): + app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="frontend") diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..3df3349 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +aiosqlite==0.20.0 diff --git a/deploy/backup-cronjob.yaml b/deploy/backup-cronjob.yaml new file mode 100644 index 0000000..64ee0ab --- /dev/null +++ b/deploy/backup-cronjob.yaml @@ -0,0 +1,39 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hourly-backup + namespace: oil-calculator +spec: + schedule: "0 * * * *" # Every hour + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 2 + jobTemplate: + spec: + template: + spec: + containers: + - name: backup + image: registry.oci.euphon.net/oil-calculator:latest + command: + - sh + - -c + - | + BACKUP_DIR=/data/backups + mkdir -p $BACKUP_DIR + DATE=$(date +%Y%m%d_%H%M%S) + # Backup SQLite database using .backup for consistency + sqlite3 /data/oil_calculator.db ".backup '$BACKUP_DIR/oil_calculator_${DATE}.db'" + echo "Backup done: $BACKUP_DIR/oil_calculator_${DATE}.db ($(du -h $BACKUP_DIR/oil_calculator_${DATE}.db | cut -f1))" + # Keep last 48 backups (2 days of hourly) + ls -t $BACKUP_DIR/oil_calculator_*.db | tail -n +49 | xargs rm -f 2>/dev/null + echo "Backups retained: $(ls $BACKUP_DIR/oil_calculator_*.db | wc -l)" + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + persistentVolumeClaim: + claimName: oil-calculator-data + restartPolicy: OnFailure + imagePullSecrets: + - name: regcred diff --git a/deploy/cronjob.yaml b/deploy/cronjob.yaml new file mode 100644 index 0000000..e5825bf --- /dev/null +++ b/deploy/cronjob.yaml @@ -0,0 +1,30 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: weekly-review + namespace: oil-calculator +spec: + schedule: "0 9 * * 1" # Every Monday 9:00 UTC (17:00 China time) + jobTemplate: + spec: + template: + spec: + containers: + - name: cron + image: curlimages/curl:latest + command: + - sh + - -c + - | + curl -sf -X POST \ + -H "Authorization: Bearer $(ADMIN_TOKEN)" \ + -H "Content-Type: application/json" \ + -d '{}' \ + http://oil-calculator.oil-calculator.svc/api/cron/weekly-review + env: + - name: ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: oil-calculator-secrets + key: admin-token + restartPolicy: OnFailure diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml new file mode 100644 index 0000000..cc00512 --- /dev/null +++ b/deploy/deployment.yaml @@ -0,0 +1,47 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oil-calculator + namespace: oil-calculator +spec: + replicas: 1 + selector: + matchLabels: + app: oil-calculator + template: + metadata: + labels: + app: oil-calculator + spec: + imagePullSecrets: + - name: regcred + containers: + - name: oil-calculator + image: registry.oci.euphon.net/oil-calculator:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + env: + - name: DB_PATH + value: /data/oil_calculator.db + - name: FRONTEND_DIR + value: /app/frontend + - name: ADMIN_TOKEN + valueFrom: + secretKeyRef: + name: oil-calculator-secrets + key: admin-token + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "500m" + volumes: + - name: data + persistentVolumeClaim: + claimName: oil-calculator-data diff --git a/deploy/ingress.yaml b/deploy/ingress.yaml new file mode 100644 index 0000000..6c3f161 --- /dev/null +++ b/deploy/ingress.yaml @@ -0,0 +1,23 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: oil-calculator + namespace: oil-calculator + annotations: + traefik.ingress.kubernetes.io/router.tls.certresolver: le +spec: + ingressClassName: traefik + rules: + - host: oil.oci.euphon.net + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: oil-calculator + port: + number: 80 + tls: + - hosts: + - oil.oci.euphon.net diff --git a/deploy/namespace.yaml b/deploy/namespace.yaml new file mode 100644 index 0000000..72d25bb --- /dev/null +++ b/deploy/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: oil-calculator diff --git a/deploy/pvc.yaml b/deploy/pvc.yaml new file mode 100644 index 0000000..eba04a4 --- /dev/null +++ b/deploy/pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: oil-calculator-data + namespace: oil-calculator +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi diff --git a/deploy/service.yaml b/deploy/service.yaml new file mode 100644 index 0000000..c9aed0e --- /dev/null +++ b/deploy/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: oil-calculator + namespace: oil-calculator +spec: + selector: + app: oil-calculator + ports: + - port: 80 + targetPort: 8000 diff --git a/deploy/setup-kubeconfig.sh b/deploy/setup-kubeconfig.sh new file mode 100644 index 0000000..c166a3e --- /dev/null +++ b/deploy/setup-kubeconfig.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Creates a restricted kubeconfig for the oil-calculator namespace only. +# Run on the k8s server as a user with cluster-admin access. +set -e + +NAMESPACE=oil-calculator +SA_NAME=oil-calculator-deployer + +echo "Creating ServiceAccount, Role, and RoleBinding..." + +kubectl apply -f - < kubeconfig <🌿 diff --git a/frontend/icon-192.png b/frontend/icon-192.png new file mode 100644 index 0000000..5759a19 Binary files /dev/null and b/frontend/icon-192.png differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9a1ecb8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,8441 @@ + + + + + + + + + + + +doTERRA 配方计算器 + + + + + + + + +
+
+
🌿
+
+

+ doTERRA 配方计算器 + +

+

查询配方·计算成本·自制配方·导出卡片·精油知识

+
+
+
+ + + +
+ + + + + +
+
🙋 我的
+ + +
+ + +
+ + +
+
+
✨ 智能粘贴(添加到我的配方)
+
+ +
+
+ + +
+
+
+
+
+ + + + + + + + + + + + +
+ + +
+
📦 我的精油库存
+ +
已有精油
+
+
可做的配方
+
+
+ + +
+
💼 商业核算
+

商业用户专属功能,包含项目核算、成本分析等工具

+ + +
+
+ 📊 服务项目成本利润分析 + +
+ + +
+ + + +
+ + + +
+ + +
+
+ 🔔 通知 + +
+
+
+ + + +
+ + +
+ + + + +
+ +
+ + +
+ 📋 配方列表 +
+ + + +
+ +
+
🏷 批量打标签
+
📤 批量分享到公共库
+
🖼 批量导出卡片
+
🗑 批量删除
+
+
+ +
+
+ + + + +
+
+ + +
+
+
✨ 智能粘贴
+
+ + +
支持逗号、顿号、换行分隔,也支持连写。空行或「配方名:」开头自动分隔为多个配方。系统会逐个让你校准后再保存。
+
+
+ + +
+
+
+ +
+
➕ 手动新增配方
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + +
+
+
+ + +
+
💧 精油价目表 种精油
+ +
+
+
💧
+
稀释比例
+
不同年龄段的稀释指南
+
+
+
⚠️
+
使用禁忌
+
安全使用精油的注意事项
+
+
+
+ + + + + +
+
+ +
+ + +
+ +
+
+
+ + +
+
📜 操作日志
+
+ + + + + + + +
+
+ 按用户: +
+
+
+
+ +
+
+ + + +
+
+ 🐛 Bug 追踪 + +
+
+
+
+ +
+
👥 用户管理
+ + + + +
+
新增用户
+
+ + + + +
+
+
+
+ + +
+
+
+ + + + + + diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..7d7a32b --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Weekly backup script - run locally or via cron +# Backs up: database from server + local code +# Keeps last 5 backups + +BACKUP_BASE="$HOME/Hera DOCS/Projects/Essential Oil Formula Cost Calculator/backups" +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="$BACKUP_BASE/$DATE" + +mkdir -p "$BACKUP_DIR" + +echo "📦 Backing up to: $BACKUP_DIR" + +# 1. Download database from server +echo " Downloading database..." +ssh fam@oci.euphon.net "kubectl exec -n oil-calculator deploy/oil-calculator -- cat /data/oil_calculator.db" > "$BACKUP_DIR/oil_calculator.db" 2>/dev/null + +if [ $? -eq 0 ] && [ -s "$BACKUP_DIR/oil_calculator.db" ]; then + echo " ✅ Database: $(du -h "$BACKUP_DIR/oil_calculator.db" | cut -f1)" +else + echo " ❌ Database download failed" +fi + +# 2. Copy local code +echo " Copying code..." +PROJECT="$HOME/Hera DOCS/Projects/Essential Oil Formula Cost Calculator" +cp -r "$PROJECT/backend" "$BACKUP_DIR/" +cp -r "$PROJECT/frontend" "$BACKUP_DIR/" +cp "$PROJECT/Dockerfile" "$BACKUP_DIR/" +echo " ✅ Code copied" + +# 3. Export all data as JSON (recipes, oils, users) +echo " Exporting data..." +ADMIN_TOKEN=$(cat /tmp/oil_admin_token.txt 2>/dev/null) +if [ -n "$ADMIN_TOKEN" ]; then + curl -s -H "Authorization: Bearer $ADMIN_TOKEN" https://oil.oci.euphon.net/api/recipes > "$BACKUP_DIR/recipes.json" 2>/dev/null + curl -s -H "Authorization: Bearer $ADMIN_TOKEN" https://oil.oci.euphon.net/api/oils > "$BACKUP_DIR/oils.json" 2>/dev/null + curl -s -H "Authorization: Bearer $ADMIN_TOKEN" https://oil.oci.euphon.net/api/tags > "$BACKUP_DIR/tags.json" 2>/dev/null + curl -s -H "Authorization: Bearer $ADMIN_TOKEN" https://oil.oci.euphon.net/api/users > "$BACKUP_DIR/users.json" 2>/dev/null + echo " ✅ Data exported" +else + echo " ⚠️ No admin token, skipping data export" +fi + +# 4. Keep only last 5 backups +echo " Cleaning old backups..." +ls -dt "$BACKUP_BASE"/*/ 2>/dev/null | tail -n +31 | xargs rm -rf 2>/dev/null +REMAINING=$(ls -d "$BACKUP_BASE"/*/ 2>/dev/null | wc -l) +echo " ✅ $REMAINING backups retained" + +echo "" +echo "✅ Backup complete: $BACKUP_DIR" +ls -la "$BACKUP_DIR/" diff --git a/scripts/remote-backup.sh b/scripts/remote-backup.sh new file mode 100755 index 0000000..96831da --- /dev/null +++ b/scripts/remote-backup.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Remote backup: download database from server to local Mac +# Run via cron: crontab -e → 0 * * * * /path/to/remote-backup.sh + +BACKUP_DIR="$HOME/Hera DOCS/Projects/Essential Oil Formula Cost Calculator/backups/remote" +mkdir -p "$BACKUP_DIR" +DATE=$(date +%Y%m%d_%H%M%S) + +# Download database +ssh fam@oci.euphon.net "kubectl exec -n oil-calculator deploy/oil-calculator -- cat /data/oil_calculator.db" > "$BACKUP_DIR/oil_calculator_${DATE}.db" 2>/dev/null + +if [ $? -eq 0 ] && [ -s "$BACKUP_DIR/oil_calculator_${DATE}.db" ]; then + echo "✅ Backup: $BACKUP_DIR/oil_calculator_${DATE}.db ($(du -h "$BACKUP_DIR/oil_calculator_${DATE}.db" | cut -f1))" + # Keep last 168 backups (7 days hourly) + ls -t "$BACKUP_DIR"/oil_calculator_*.db 2>/dev/null | tail -n +169 | xargs rm -f 2>/dev/null +else + echo "❌ Backup failed" + rm -f "$BACKUP_DIR/oil_calculator_${DATE}.db" +fi diff --git a/scripts/scan-recipes-folder.py b/scripts/scan-recipes-folder.py new file mode 100644 index 0000000..e60913d --- /dev/null +++ b/scripts/scan-recipes-folder.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +""" +Scan the local recipe folder for new Excel files and extract recipes. +Run periodically (e.g., weekly) to find new content to add to the calculator. + +Usage: + python3 scripts/scan-recipes-folder.py + +It will: +1. Read all .xlsx files in the recipe folder +2. Compare with existing recipes in the API +3. Report any new recipes found +4. Optionally upload them (with --upload flag) +""" +import os +import sys +import json +import argparse + +RECIPE_FOLDER = os.path.expanduser("~/Hera DOCS/Essential Oil/商业和公司/成本和配方") +API_BASE = "https://oil.oci.euphon.net" + +def main(): + parser = argparse.ArgumentParser(description="Scan recipe folder for new content") + parser.add_argument("--upload", action="store_true", help="Upload new recipes to API") + parser.add_argument("--token", help="Admin API token") + args = parser.parse_args() + + print(f"📁 Scanning: {RECIPE_FOLDER}") + if not os.path.isdir(RECIPE_FOLDER): + print(f" Folder not found!") + return + + files = [f for f in os.listdir(RECIPE_FOLDER) if f.endswith('.xlsx') and not f.startswith('~$')] + print(f" Found {len(files)} Excel files:") + for f in files: + mtime = os.path.getmtime(os.path.join(RECIPE_FOLDER, f)) + from datetime import datetime + print(f" - {f} (modified: {datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M')})") + + print(f"\n💡 To add recipes from these files, use the web UI's '智能粘贴' feature") + print(f" or manually add them via the API.") + print(f"\n🔗 Admin URL: {API_BASE}/?token=") + +if __name__ == "__main__": + main() diff --git a/精油配方计算器.html b/精油配方计算器.html new file mode 100644 index 0000000..ad21430 --- /dev/null +++ b/精油配方计算器.html @@ -0,0 +1,2057 @@ + + + + + +🌿 精油配方计算器 + + + + + + + +
+
+
🌿
+
+

DOTERRA 精油配方计算器(Hera)

+

查询配方 · 计算成本 · 导出卡片

+
+
+
+ + + +
+ + + + + +
+ + + +
+ 📋 配方列表 +
+ + +
+
+
+
+ + +
+
+
✨ 智能粘贴
+
+ + +
支持逗号、顿号、换行分隔,也支持无分隔直接连写(如:芳香调理8永久花10檀香10),自动用数字断句。不需要写椰子油,系统会按稀释比例自动计算。
+
+
+ + +
+
+
+ +
+
➕ 手动新增配方
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + +
+
+
+ + +
+
💧 精油价目表 种精油
+
+ + + + + +
+ +
+
+ +
+ + + +