Răsfoiți Sursa

feat(汇安融): 门店编号自动生成 —— SP + 时间戳 + 16 位 CSPRNG 随机段

运营手填 third_shop_id 的代价不对称:撞号 ⇒ 上游按「重复提交」整笔拒建;
抄错一个字符 ⇒ 上游没有门店更新接口,事后补不回来。改成打开建店弹窗时自动生成。

- utils/shop-code.ts:SP + yyyyMMddHHmmss + 16 位随机,共 32 字符吃满 varchar(32);
  字母表 32 个去 0/1/I/O,32 是 2 的幂 ⇒ 取随机字节低 5 位无偏;随机源
  crypto.getRandomValues(编号是上游幂等键,Math.random 不可接受)
- 建店表单:编号格只读 + 生成说明;openCreate 每次打开生成新号
- 执行级台子(logs/,不进库)11 条断言含 CSPRNG 调用探针;两向变异验证
  (换 Math.random 只有探针红;&31→%32 等价改写仍绿)
alphaH 5 zile în urmă
părinte
comite
128a2276de

+ 28 - 0
frontend/src/utils/shop-code.ts

@@ -0,0 +1,28 @@
+import dayjs from "dayjs";
+
+/**
+ * 门店编号(third_shop_id)自动生成 —— 汇安融门店创建表单用。
+ *
+ * 形状:`SP` + `yyyyMMddHHmmss` + 16 位随机,共 32 字符,正好吃满
+ * `third_shop_id` 的 varchar(32)。改成自动生成是因为运营手填这个编号的代价不对称:
+ * 撞号 ⇒ 上游按「重复提交」拒绝建店;填错一个字符 ⇒ 上游没有更新接口,事后补不上。
+ *
+ * 两个刻意的选择:
+ * - 字母表去掉 0/1/I/O(连同数字段共 32 个字符):编号要能在电话/微信里念得出来,
+ *   去掉易混字符,一个歧义就少一类抄错。
+ * - 32 是 2 的幂 ⇒ 取随机字节的低 5 位做下标是无偏的(256 % 32 == 0),无需拒绝采样。
+ *   随机源必须是 crypto.getRandomValues:这个编号在上游是**幂等键**,撞号等于
+ *   整笔建店被拒,Math.random 的碰撞概率和可预测性都不能接受。
+ */
+const ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
+
+export function generateShopCode(now: Date = new Date()): string {
+  const stamp = dayjs(now).format("YYYYMMDDHHmmss");
+  const bytes = new Uint8Array(16);
+  crypto.getRandomValues(bytes);
+  let rand = "";
+  for (const b of bytes) {
+    rand += ALPHABET[b & 31];
+  }
+  return `SP${stamp}${rand}`;
+}

+ 12 - 2
frontend/src/views/module_payment/har/shop/index.vue

@@ -197,12 +197,19 @@
           </el-select>
         </el-form-item>
         <el-form-item label="门店编码" prop="third_shop_id">
+          <!-- 自动生成(2026-10-09):编号在上游是**幂等键** —— 撞号等于整笔建店被拒,
+               手填还容易抄错;而上游没有门店更新接口,错一个字符这家店就补不回来。
+               只读展示、不改动。 -->
           <el-input
             v-model="form.third_shop_id"
-            placeholder="商户侧门店唯一编码"
+            readonly
+            placeholder="打开时自动生成"
             maxlength="32"
-            show-word-limit
           />
+          <div class="form-tip">
+            编号由系统自动生成(SP + 时间 + 随机段),一旦提交不可再改 ——
+            上游没有门店更新接口。
+          </div>
         </el-form-item>
         <el-form-item label="门店名称" prop="shop_name">
           <el-input v-model="form.shop_name" maxlength="128" show-word-limit />
@@ -297,6 +304,7 @@ import HarAPI, {
   type HarShopVO,
 } from "@/api/module_payment/har";
 import { useEnterpriseStore } from "@/store/modules/enterprise.store";
+import { generateShopCode } from "@/utils/shop-code";
 
 const enterpriseStore = useEnterpriseStore();
 
@@ -479,6 +487,8 @@ function handleReset() {
 
 function openCreate() {
   Object.assign(form, defaultForm());
+  // 每次打开都生成新号:编号在上游是幂等键,手填易撞号/抄错,且事后补不上
+  form.third_shop_id = generateShopCode();
   createVisible.value = true;
   // 目录是异步来的(也只在真正建店时用得上),别放进 onMounted 让列表页白等
   ensureProvinces();