算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
elixir【设计与创意】Use this skill when building with Elixir — Phoenix framework, OTP, BEAM, LiveView, Ecto, supervision trees. This skill enforces: OTP design principles, Phoenix context boundaries, LiveView state management, Ecto schema conventions, supervision tree structure. Do NOT use for: non-Elixir projects, frontend JavaScript, simple scripts better suited for B…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Build fault-tolerant, concurrent applications with Elixir/OTP — Phoenix web layer, LiveView interactivity, Ecto persistence, supervision trees, process architecture.
User request includes: Elixir, Phoenix, Phoenix LiveView, OTP, BEAM, Elixir macros, Ecto, mix, phx.gen, Supervisor, GenServer, Phoenix channels, Phoenix PubSub, Elixir processes, iex.
Phoenix project structure, OTP supervision tree, Ecto schema, LiveView module, router config.
Produce artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.
4096 tokens
# Install Phoenix
mix archive.install hex phx_new
# Create new Phoenix app
mix phx.new my_app --database postgres
mix phx.new my_app --database sqlite3 # Alternative
# Setup database
mix ecto.create
mix ecto.migrate
# Start server
mix phx.server
my_app/
lib/
my_app/
application.ex # OTP application start
repo.ex # Ecto Repo
accounts/ # Context: accounts
user.ex # Ecto schema
user_notifier.ex # Boundary call
user_token.ex
accounts.ex # Context module (public API)
catalog/ # Context: catalog
product.ex
category.ex
catalog.ex
store/ # Context: orders
order.ex
line_item.ex
store.ex
web/
endpoint.ex # Phoenix endpoint
router.ex # Router
controllers/ # Controllers (non-LiveView)
user_session_controller.ex
live/ # LiveViews
product_live/
index.ex
show.ex
cart_live/
index.ex
components/ # Shared components
layout.ex # App layout
navbar.ex
product_card.ex
templates/ # Templates (non-LiveView)
layout/
mailer.ex # Mailer (Swoosh/Bamboo)
my_app.ex # Module aliases
priv/
repo/
migrations/
20250101000000_create_users.exs
config/
config.exs
dev.exs
prod.exs
runtime.exs
mix.exs
# priv/repo/migrations/20250101000000_create_users.exs
defmodule MyApp.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users, primary_key: false) do
add :id, :uuid, primary_key: true, default: fragment("gen_random_uuid()")
add :email, :string, null: false
add :username, :string, null: false
add :hashed_password, :string, null: false
add :role, :string, default: "user"
add :confirmed_at, :naive_datetime
add :deleted_at, :naive_datetime
timestamps()
end
create unique_index(:users, [:email])
create unique_index(:users, [:username])
create index(:users, [:deleted_at])
end
end
# lib/my_app/accounts/user.ex
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "users" do
field :email, :string
field :username, :string
field :role, :string, default: "user"
field :confirmed_at, :naive_datetime
field :deleted_at, :naive_datetime
has_many :orders, MyApp.Store.Order
has_one :profile, MyApp.Accounts.Profile
timestamps()
end
@doc false
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :username, :role])
|> validate_required([:email, :username])
|> validate_format(:email, ~r/@/)
|> validate_length(:username, min: 3, max: 30)
|> unique_constraint(:email)
|> unique_constraint(:username)
end
end
# lib/my_app/accounts/accounts.ex
defmodule MyApp.Accounts do
@moduledoc """
Accounts context — user registration, authentication, profile management.
"""
import Ecto.Query, warn: false
alias MyApp.Repo
alias MyApp.Accounts.{User, UserToken, UserNotifier}
@doc """
Registers new user.
"""
def register_user(attrs) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
@doc """
Returns user by id.
"""
def get_user!(id), do: Repo.get!(User, id)
@doc """
Authenticates user by email and password.
"""
def authenticate_by_email(email, password) do
user = Repo.get_by(User, email: String.downcase(email))
case check_password(user, password) do
true -> {:ok, user}
false -> {:error, :invalid_credentials}
end
end
defp check_password(nil, _password), do: false
defp check_password(user, password) do
Argon2.verify_pass(password, user.hashed_password)
end
@doc """
Lists all active users.
"""
def list_users do
Repo.all(from u in User, where: is_nil(u.deleted_at), order_by: u.inserted_at)
end
end
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug MyAppWeb.Plugs.Authenticate
end
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.Plugs.ApiAuth
end
# Browser routes
scope "/", MyAppWeb do
pipe_through :browser
get "/", PageController, :index
get "/login", UserSessionController, :new
post "/login", UserSessionController, :create
delete "/logout", UserSessionController, :delete
live "/products", ProductLive.Index, :index
live "/products/new", ProductLive.Index, :new
live "/products/:id/edit", ProductLive.Index, :edit
live "/products/:id", ProductLive.Show, :show
end
# Authenticated routes
scope "/", MyAppWeb do
pipe_through [:browser, :require_authenticated]
live "/dashboard", DashboardLive, :index
live "/cart", CartLive.Index, :index
live "/orders", OrderLive.Index, :index
end
# API routes
scope "/api/v1", MyAppWeb do
pipe_through :api
post "/users", Api.UserController, :create
post "/sessions", Api.SessionController, :create
get "/products", Api.ProductController, :index
end
end
# lib/my_app_web/live/product_live/index.ex
defmodule MyAppWeb.ProductLive.Index do
use MyAppWeb, :live_view
alias MyApp.Catalog
alias MyApp.Catalog.Product
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign(:page_title, "Products")
|> stream(:products, Catalog.list_products())
|> assign(:form, to_form(%{search: ""}))
{:ok, socket}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit Product")
|> assign(:product, Catalog.get_product!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New Product")
|> assign(:product, %Product{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Products")
|> assign(:product, nil)
end
@impl true
def handle_event("search", %{"search" => query}, socket) do
products = Catalog.search_products(query)
{:noreply, stream(socket, :products, products, reset: true)}
end
@impl true
def handle_info({MyAppWeb.ProductLive.Index, [:product_updated]}, socket) do
{:noreply, stream(socket, :products, Catalog.list_products(), reset: true)}
end
end
# lib/my_app_web/live/product_live/index.html.heex
<div class="product-list">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Products</h1>
<.link navigate={~p"/products/new"}>
<.button>New Product</.button>
</.link>
</div>
<.form for={@form} phx-change="search" class="mb-4">
<.input field={@form[:search]} placeholder="Search products..." />
</.form>
<.table
id="products"
rows={@streams.products}
row_click={fn {_id, product} -> navigate(~p"/products/#{product}")}
>
<:col :let={{_id, product}} label="Name"><%= product.name %></:col>
<:col :let={{_id, product}} label="Price"><%= product.price %></:col>
<:col :let={{_id, product}} label="Stock"><%= product.stock_count %></:col>
<:action :let={{_id, product}}>
<.link navigate={~p"/products/#{product}/edit"}>Edit</.link>
</:action>
</.table>
</div>
# lib/my_app/application.ex
defmodule MyApp.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Start Ecto repo
MyApp.Repo,
# Start Telemetry
{Phoenix.PubSub, name: MyApp.PubSub},
# Start Phoenix endpoint
MyAppWeb.Endpoint,
# Start workers
MyApp.Workers.ProductCache,
MyApp.Workers.EmailQueue,
MyApp.Workers.SessionCleaner,
# Start Oban for background jobs
{Oban, oban_config()},
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
defp oban_config do
Oban.Config.new(
repo: MyApp.Repo,
queues: [default: 10, emails: 5, cleanup: 1],
prune: :active,
)
end
end
# lib/my_app/workers/product_cache.ex
defmodule MyApp.Workers.ProductCache do
use GenServer
@cache_ttL :timer.minutes(5)
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
@impl true
def init(state) do
# Schedule initial cache refresh
send(self(), :refresh)
{:ok, state}
end
@impl true
def handle_info(:refresh, state) do
products = MyApp.Catalog.list_products()
:ets.new(:product_cache, [:named_table, :public, read_concurrency: true])
:ets.insert(:product_cache, {:products, products})
Process.send_after(self(), :refresh, @cache_ttL)
{:noreply, state}
end
def get_products do
case :ets.lookup(:product_cache, :products) do
[{:products, products}] -> products
[] -> MyApp.Catalog.list_products()
end
end
end
State management pattern?
├── Mutable state with side effects → GenServer
│ Confirms that you need state transitions with side effects (sending emails, writing to DB).
│ Use handle_call for synchronous, handle_cast for fire-and-forget.
├── Simple state, no side effects → Agent
│ get/update pattern. No handle_call/cast boilerplate. Good for config, counters.
│ Risk: Agent.update is synchronous — wraps GenServer.call internally.
└── Read-heavy, rarely-changed → ETS
No process bottleneck. ~1µs reads. Good for reference data, session caches.
Risk: data lost on restart. Pair with Ecto for durability.
Failure recovery requirement?
├── Children independent → :one_for_one
├── Dependency chain → :rest_for_one
│ If DB worker dies, restart DB before workers that depend on it.
├── All must be up → :one_for_all
│ If any child dies, restart all. Use for tightly coupled services.
└── Dynamic children → DynamicSupervisor
For GenServers started at runtime (e.g., per-user processes).
# lib/my_app/catalog.ex
defmodule MyApp.Catalog do
alias MyApp.Repo
alias MyApp.Catalog.Product
def create_product(attrs) do
%Product{}
|> Product.changeset(attrs)
|> Repo.insert()
|> broadcast(:product_created)
end
defp broadcast({:ok, product}, event) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "products", {event, product})
{:ok, product}
end
defp broadcast(error, _event), do: error
end
# lib/my_app_web/live/product_live/index.ex (mount)
def mount(_params, _session, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "products")
{:ok, stream(socket, :products, Catalog.list_products())}
end
def handle_info({:product_created, product}, socket) do
{:noreply, stream_insert(socket, :products, product, at: 0)}
end
def place_order(cart_id, customer_id) do
Ecto.Multi.new()
|> Ecto.Multi.run(:cart, fn _repo, _changes ->
case get_cart(cart_id) do
%Cart{status: :active} = cart -> {:ok, cart}
_ -> {:error, :cart_not_found}
end
end)
|> Ecto.Multi.run(:order, fn repo, %{cart: cart} ->
%Order{customer_id: customer_id, total: cart.total}
|> Order.changeset(%{})
|> repo.insert()
end)
|> Ecto.Multi.run(:line_items, fn repo, %{order: order} ->
items = Enum.map(cart.items, fn item ->
%LineItem{order_id: order.id, product_id: item.product_id, quantity: item.quantity, price: item.price}
end)
repo.insert_all(LineItem, items)
end)
|> Ecto.Multi.run(:clear_cart, fn repo, _changes ->
repo.delete_all(from c in CartItem, where: c.cart_id == ^cart_id)
end)
|> Repo.transaction()
end
mix release build → container image → CI tests → staging deploy → smoke tests → prod deploy
MIX_ENV=prod mix release bundles BEAM + deps + config into self-contained directory.RELEASE_NODE and RELEASE_COOKIE for distributed Erlang node clustering./health endpoint returns 200 when app is running. DB connectivity checked before startup completes.[:phoenix, :endpoint, :stop] for request duration, [:ecto, :query, :total] for DB./metrics endpoint scraped by Prometheus.Logger.metadata() with request_id, user_id, tenant_id for structured logs.| Anti-Pattern | Why It Hurts | Fix |
|---|---|---|
| GenServer as DB replacement | State lost on crash, restart resets. No durability. | Use Ecto for persistent data; GenServer for cache/state only |
IO.inspect debugging in prod | Compiles into release. Exposes internal state in logs. | Logger.debug(inspect(data)) removed by compiler in :prod |
| Phoenix context cross-calls | Context A calls Context B's Repo directly. Breaks encapsulation. | Always go through public context API functions |
| Large Ecto preloads in LiveView | 10+ preloads per query = slow mounts. | Paginate, lazy-load, or use ETS cache for reference data |
send_after without cancellation | Timer survives GenServer crash. Fires into dead process. | Store timer ref, cancel on terminate with Process.cancel_timer/1 |
:infinity GenServer timeout | Blocks caller indefinitely. Cascading failure on stuck call. | Always set explicit timeout (default 5000ms) |
Stream.map / Stream.filter avoid intermediate list allocation.:named_table, :public, read_concurrency: true for read-heavy workloads.Repo.preload with select: [:id, :name] to fetch only needed columns.stream_insert / stream_delete over full re-render for list performance.IO.iodata_to_binary/1 for string concatenation in hot paths — avoids binary copying.:persistent_term for truly immutable configuration — fastest read on BEAM (~10ns).Cachex.fetch(:products, key, fn -> Catalog.get_product(id) end) with TTL.:put_secure_browser_headers plug enabled by default. Don't disable.cast/3. Never use cast(attrs, [:all]).Argon2 (memory-hard) over bcrypt. Never store plaintext or MD5/SHA.Plug.CORS with explicit origins. Never Access-Control-Allow-Origin: * for auth endpoints.Ecto.Adapters.SQL.query! with string interpolation.mix hex.audit in CI. Pin versions in mix.lock.