一覧SD-12-1 · 業務ロジックと競合の防御

注文内容の検証と承認

承認済みという一つのフラグだけで済ませず、承認対象と確定した記録を対応付けます。

説明

購入処理では、数量、単価、承認対象、確定状態をサーバー側で対応付けます。ログイン済みであることだけでは、内容を変えた注文の確定まで許したことにはなりません。

単価100円の商品を使い、クライアントの申告価格、数量の境界、承認後の変更を順に試します。承認した主体・商品・数量・単価・版を保存し、確定時に同じ内容か照合します。

承認済みという一つのフラグだけで済ませず、承認対象と確定した記録を対応付けます。

1サーバーが持つ商品価格と注文を読む

注文1はaliceの商品7、数量2です。単価はcatalogから取得します。

from sd12 import * lab = Orders() print(lab.view()注文とサーバー単価を結合して読む)

順に注文ID、主体、商品、数量、単価、合計、版、状態です。合計は2×100=200、状態はdraftです。

2申告価格を変えてもサーバーで金額を決める

数量を3、申告価格を1円として渡します。

print(lab.change(3,client_price=1低い申告価格を渡す)) print(lab.view()保存された数量と計算結果を読む)

合計は3×100=300です。client_priceは計算に使いません。表示用の価格を送らせる場合も、確定時の正本をサーバー側で決めます。

3数量の値と型を検査する

負数、上限超過、真偽値を試します。

for qty in (-1,6,True)業務上限と型の境界を試す: print(repr(qty),lab.change(qty)) print(lab.view())

すべてinvalid_quantityで数量3、版2のままです。負数による金額反転や、真偽値の整数扱いを入口で避けます。

4承認前の確定と他者の承認を拒否する

注文を作っただけでは確定できません。

print(lab.confirm("alice")承認を飛ばした確定を試す) print(lab.approve("bob")別主体からの承認を試す) print(lab.db.execute("SELECT COUNT(*) FROM paid").fetchone())

approval_mismatchとsubject_rejectedで、支払記録は0件です。主体と工程の両方を確認します。

5現在の内容を承認して照合値を保存する

aliceの承認を、対象内容のハッシュへ結び付けます。

print(lab.approve("alice")現在の対象内容を承認として保存する) print(lab.db.execute("SELECT length(approved),approved=?現在の内容との一致を照合する FROM orders",(lab.fingerprint(),)).fetchone())

64桁の照合値が現在内容と一致します。このハッシュはサーバーの信頼するDBに保存する比較値で、ハッシュ単体が承認者の署名になるわけではありません。

6承認後の変更には再承認を求める

数量を4へ変更します。変更処理は旧承認を消し、版を進めます。

print(lab.change(4)承認した内容を変更する) print(lab.confirm("alice")旧承認での確定を試す) print(lab.view()) print(lab.db.execute("SELECT COUNT(*) FROM paid").fetchone())

合計400、版3となり、確定はapproval_mismatchです。300円の注文への承認を400円の確定に流用していません。

7再承認した内容を一度だけ確定する

変更後を承認し、同じトランザクション内で照合と確定を行います。

print(lab.approve("alice"),lab.confirm("alice")変更後の内容を承認して確定する) print(lab.confirm("alice"),lab.change(2)) print(lab.db.execute("SELECT * FROM paid確定額と件数を確認する").fetchall())

400円が1件だけ保存され、再確定と確定後変更はstate_rejectedです。失敗後も確定済みの記録を保っています。

まとめ

価格はサーバーの正本から計算します。

承認を主体・対象・数量・金額・版へ結び付けます。

内容変更後は再承認し、確定を一度に制限します。

操作履歴と解説を振り返れます。

Python 3.15 / 固定の公開試料

1
from sd12 import * lab = Orders() print(lab.view())
(1, 'alice', 7, 2, 100, 200, 1, 'draft')
2
print(lab.change(3,client_price=1)) print(lab.view())
changed
(1, 'alice', 7, 3, 100, 300, 2, 'draft')
3
for qty in (-1,6,True): print(repr(qty),lab.change(qty)) print(lab.view())
-1 invalid_quantity
6 invalid_quantity
True invalid_quantity
(1, 'alice', 7, 3, 100, 300, 2, 'draft')
4
print(lab.confirm("alice")) print(lab.approve("bob")) print(lab.db.execute("SELECT COUNT(*) FROM paid").fetchone())
approval_mismatch
subject_rejected
(0,)
5
print(lab.approve("alice")) print(lab.db.execute("SELECT length(approved),approved=? FROM orders",(lab.fingerprint(),)).fetchone())
approved
(64, 1)
6
print(lab.change(4)) print(lab.confirm("alice")) print(lab.view()) print(lab.db.execute("SELECT COUNT(*) FROM paid").fetchone())
changed
approval_mismatch
(1, 'alice', 7, 4, 100, 400, 3, 'draft')
(0,)
7
print(lab.approve("alice"),lab.confirm("alice")) print(lab.confirm("alice"),lab.change(2)) print(lab.db.execute("SELECT * FROM paid").fetchall())
approved paid
state_rejected state_rejected
[(1, 400)]

演習完了