Compare commits

...

6 Commits

Author SHA1 Message Date
wangjia 0d967e899a chore: release client-v1.0.59
Deploy Client / build-client-web (push) Successful in 41s
Deploy Client / build-windows (push) Successful in 2m36s
Deploy Client / build-macos (push) Successful in 2m17s
Deploy Client / build-android (push) Successful in 1m36s
Deploy Client / build-ios (push) Successful in 2m46s
Deploy Client / release-deploy-client (push) Successful in 1m24s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
2026-06-19 14:07:16 +08:00
wangjia 0a21ea5b2a chore: release server-v1.0.61
Deploy Server / release-deploy-server (push) Successful in 1m8s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
2026-06-19 14:06:03 +08:00
wangjia 9b6dba4c2b test(backend): 补 redeem_code 单测 + gitignore todo/ 与 gencode 产物
覆盖 NormalizeCode 边界/幂等、GenerateRedeemCode 格式/无歧义字母表
(剔除 0/O/1/I/L)/无碰撞。.gitignore 忽略全局 todo skill 本地数据
与 backend/gencode 构建产物。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
2026-06-19 14:04:22 +08:00
wangjia 54451f0e52 feat(deploy): gencode 纳入 server 发版产物 + EC2 生码封装
server 流水线编译并发布 jiu-gencode 二进制(回滚路径随 release 资产
自动下载),部署到 /opt/jiu/backend/,对旧版回滚做存在性判断优雅跳过。
新增 deploy/jiu-gencode.sh 封装自动读 production.env,线上生码从此
一条 ssh 即可,无需本地交叉编译 + scp。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
2026-06-19 14:04:14 +08:00
wangjia 8fc24b5d36 chore(deploy): 移除退役的 ed25519/HMAC license 死配置
兑换券改造后运行时已不读签名相关 env,清理 production.env.template
的 LICENSE_HMAC_SECRET / LICENSE_ED25519_* 三项,并从 render-env.sh
的 SECRET_KEYS 去掉对应密钥(渲染 env 时不再白拉 Bitwarden)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
2026-06-19 14:04:06 +08:00
wangjia 6ac6b52717 chore: 移除两个废弃 skill(archive-design / design-review)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
2026-06-19 12:52:30 +08:00
12 changed files with 171 additions and 285 deletions
-152
View File
@@ -1,152 +0,0 @@
---
name: archive-design
description: 将已确认上线的 Figma 设计稿存档到 docs/design/archive/。接收 Figma 文件链接,通过 Figma MCP 读取设计内容,自动整理并写入存档文档。在设计确认、功能上线后调用。
allowed-tools: Read, Write, Glob, Grep
user-invocable: true
argument-hint: <figma-url> <功能名称> [版本号,默认v1]
---
# 设计存档工作流
你正在执行 `/archive-design` 命令,将已确认的 Figma 设计存档。
## 参数解析
```
$ARGUMENTS 格式:<figma-url> <功能名称> [版本]
示例:https://figma.com/file/xxx 入库单 v1
```
`$ARGUMENTS` 中提取:
- `FIGMA_URL`Figma 文件链接
- `FEATURE_NAME`:功能名称(用于目录和文件命名)
- `VERSION`:版本号,未提供则默认 `v1`
## 执行步骤
### Step 1:读取 Figma 文件
使用 Figma MCP 工具读取设计文件内容:
- 获取所有页面列表
- 获取主要页面的组件结构
- 获取设计 Token(颜色、文字样式、间距)
- 获取组件列表和说明
如果 Figma MCP 不可用,跳过此步骤,仅记录链接和手动填写的信息。
### Step 2:读取已有需求和架构文档
读取以下文件,补充存档上下文:
- `docs/requirements/{功能名称}.md`(如存在)
- `docs/api/{功能名称}.md`(如存在)
- `docs/design/{功能名称}-design-brief.md`(如存在)
### Step 3:写入存档文档
创建 `docs/design/archive/{功能名称}/{版本}/design-archive.md`
```markdown
# {功能名称} — 设计存档 {版本}
**存档时间**{当前日期}
**Figma 文件**{FIGMA_URL}
**状态**:✅ 已确认上线
---
## 设计概览
### 页面清单
| 页面名称 | Figma 画板 | 说明 |
|---------|-----------|------|
| (从 Figma 读取或手动填写)|
### 核心设计决策
(从 docs/design/{功能名称}-design-brief.md 中提取设计目标和关键决策)
---
## 设计规范
### 颜色使用
```yaml
(从 Figma 读取,或按项目基础规范填写)
主色: #1565C0 用于:顶栏、主按钮、链接
成功色: #2E7D32 用于:已审核状态标签
危险色: #C62828 用于:删除操作、库存不足警告
```
### 组件清单
```yaml
(列出本功能使用的主要组件)
复用现有组件:
- AppDataTable:通用数据表格
- StatusBadge:状态标签
- FormDialog:弹窗表单
新增组件:
- (从 Figma 读取或手动填写)
```
### 关键尺寸
```yaml
(从 Figma 读取,或手动填写关键布局尺寸)
弹窗宽度: 720px
表格行高: 48px
工具栏高: 52px
```
---
## 交互说明
(描述关键交互逻辑,如状态流转、操作确认弹窗等)
---
## 与需求的对应关系
| 用户故事 | 设计实现 |
|---------|---------|
| (从需求文档提取,对应到具体设计页面)|
---
## 历史版本
| 版本 | 时间 | 变更说明 |
|------|------|---------|
| {版本} | {当前日期} | 初始版本 |
```
### Step 4:更新设计索引
在 `docs/design/archive/INDEX.md` 中追加一行记录(如文件不存在则创建):
```markdown
# 设计存档索引
| 功能 | 版本 | 存档时间 | Figma 链接 | 存档文档 |
|------|------|---------|-----------|---------|
| {功能名称} | {版本} | {日期} | [查看]({FIGMA_URL}) | [文档](/{功能名称}/{版本}/design-archive.md) |
```
### Step 5:完成提示
输出:
```
✅ 设计存档完成
📁 存档路径:docs/design/archive/{功能名称}/{版本}/design-archive.md
🔗 Figma 链接:{FIGMA_URL}
📋 索引已更新:docs/design/archive/INDEX.md
下一步建议:
- 运行 /design-review 对比实现与设计稿的差异
- 提交存档文件到 gitgit add docs/design/archive/
```
-127
View File
@@ -1,127 +0,0 @@
---
name: design-review
description: 对比 Figma 设计稿与 Flutter 实现代码,输出还原度差异报告。在 flutter-coder 实现完成后、上线前调用,确保实现与设计一致。
allowed-tools: Read, Write, Glob, Grep
user-invocable: true
argument-hint: <figma-url> <功能名称>
---
# 设计还原度检查工作流
你正在执行 `/design-review` 命令,对比 Figma 设计稿与 Flutter 实现的差异。
## 参数解析
```
$ARGUMENTS 格式:<figma-url> <功能名称>
示例:https://figma.com/file/xxx 入库单
```
## 执行步骤
### Step 1:读取 Figma 设计规范
使用 Figma MCP 读取:
- 各页面的组件结构和层级
- 颜色值(精确到 hex
- 字体大小、字重
- 间距数值(padding、margin、gap
- 组件状态(hover、disabled、selected 等)
如果 Figma MCP 不可用,读取 `docs/design/{功能名称}-design-brief.md` 中的设计规范作为对照基准。
### Step 2:读取 Flutter 实现代码
查找并读取相关实现文件:
```
client/lib/screens/ → 找到功能名称相关的 screen 文件
client/lib/widgets/ → 找到使用的组件文件
client/lib/models/ → 数据模型
```
重点检查:
- 颜色是否使用了正确的 hex 值或 Theme 变量
- 字体大小、字重是否与设计一致
- 间距(padding/margin)数值是否匹配
- 组件状态样式是否完整实现
- 布局结构是否与设计稿一致
### Step 3:生成差异报告
写入 `docs/review/{功能名称}-design-review.md`
```markdown
# {功能名称} — 设计还原度报告
**检查时间**{当前日期}
**Figma 设计稿**{FIGMA_URL}
**检查范围**{列出检查的 Flutter 文件}
---
## 总体评分
还原度:**X / 10**
(10分=完全一致,8分=细节有出入,6分=布局基本对但样式差异明显)
---
## 必须修复(影响视觉一致性)
### [DR-001] 颜色不一致
**页面**:入库单列表 — 状态标签
**设计稿**:已审核状态背景色 `#E8F5E9`,文字 `#2E7D32`
**实现**:背景色 `Colors.green.shade100`(实际值 `#DCEDC8`,偏浅)
**文件**`client/lib/widgets/status_badge.dart:34`
**修复**:改为 `Color(0xFFE8F5E9)`
### [DR-002] 间距不一致
**页面**:新建入库单弹窗
**设计稿**:表单字段间距 16px
**实现**`SizedBox(height: 12)` — 差了 4px
**文件**`client/lib/screens/stock_in/stock_in_form.dart:67`
**修复**:改为 `SizedBox(height: 16)`
---
## 建议调整(不影响主要功能,但影响精细度)
### [DR-003] 字体字重
**页面**:表格标题行
**设计稿**`FontWeight.w600`
**实现**`FontWeight.bold`(实际是 w700,略粗)
**建议**:统一改为 `FontWeight.w600`
---
## 通过检查项
- ✅ 整体布局结构与设计稿一致
- ✅ 主色 #1565C0 使用正确
- ✅ 顶栏高度 56px 正确
- ✅ 侧边栏宽度 200px 正确
- ✅ 状态流转交互与设计一致
---
## 修复建议
优先处理 DR-001、DR-002(颜色和间距是最直观的差异)。
修复后可再次运行 `/design-review` 验证。
```
### Step 4:完成提示
输出:
```
✅ 设计还原度检查完成
📋 报告路径:docs/review/{功能名称}-design-review.md
🔍 发现问题:必须修复 X 项,建议调整 Y 项
后续步骤:
1. 将报告发给 flutter-coder 修复(必须修复项)
2. 修复后再次运行 /design-review 验证
3. 全部通过后运行 /archive-design 存档设计稿
```
+4
View File
@@ -55,6 +55,10 @@ coverage/
# 编译产物(后端可执行文件)
backend/issue
backend/gencode
# 全局 todo skill 的本地数据(看板/任务,不入库)
/todo/
# Claude 私有文件
.claude/commands/
+11
View File
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.59] - 2026-06-19
### 新功能
- 授权到期后可在界面内直接续费:输入兑换码(形如 JIUKU-XXXX-XXXX)即可为门店叠加授权时长
### 改进
- 品牌焕新:登录页启用「岩美酒库」品牌标识,桌面 / 移动 / 网页各端应用图标统一为品牌图标
- 「激活授权」改为「兑换激活码」语义,文案与提示更贴合按时长续期的使用方式
- 授权状态实时生效:续费、到期或授权被调整后无需重新登录即可刷新;账号被停用或在其他设备登录时及时自动退出
- 「退出登录」收入右上角用户名下拉菜单,界面更聚焦
## [1.0.58] - 2026-06-18
### 修复
+5
View File
@@ -5,6 +5,11 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.61] - 2026-06-19
### 改进
- 平台侧兑换券生成工具随服务端一并发布并部署到服务器,可直接在服务器上批量生成激活码,无需每次本地编译上传;同步清理已退役的授权签名相关配置项
## [1.0.60] - 2026-06-19
### 新功能
+94
View File
@@ -0,0 +1,94 @@
package util
import (
"strings"
"testing"
)
func TestNormalizeCode(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"展示格式带连字符", "JIUKU-YD9S-7CAP", "JIUKUYD9S7CAP"},
{"小写转大写", "jiuku-yd9s-7cap", "JIUKUYD9S7CAP"},
{"去除空格", "JIUKU YD9S 7CAP", "JIUKUYD9S7CAP"},
{"去除制表/换行/回车", "JIUKU\tYD9S\n7CAP\r", "JIUKUYD9S7CAP"},
{"已归一化幂等", "JIUKUYD9S7CAP", "JIUKUYD9S7CAP"},
{"空串", "", ""},
{"混合大小写与分隔", " jiUKu--yd9s 7caP\n", "JIUKUYD9S7CAP"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := NormalizeCode(c.in); got != c.want {
t.Fatalf("NormalizeCode(%q) = %q, want %q", c.in, got, c.want)
}
})
}
}
func TestNormalizeCodeIdempotent(t *testing.T) {
// 二次归一化结果不变
once := NormalizeCode("jiuku-yd9s-7cap")
if twice := NormalizeCode(once); twice != once {
t.Fatalf("NormalizeCode 非幂等: %q -> %q", once, twice)
}
}
func TestGenerateRedeemCodeFormat(t *testing.T) {
for i := 0; i < 200; i++ {
code := GenerateRedeemCode()
// 展示格式:JIUKU-XXXX-XXXX
parts := strings.Split(code, "-")
if len(parts) != 3 {
t.Fatalf("码段数应为 3,得到 %d%q", len(parts), code)
}
if parts[0] != "JIUKU" {
t.Fatalf("前缀应为 JIUKU,得到 %q%q", parts[0], code)
}
if len(parts[1]) != 4 || len(parts[2]) != 4 {
t.Fatalf("随机段长度应各为 4,得到 %d/%d%q", len(parts[1]), len(parts[2]), code)
}
// 归一化后 = 前缀 + 8 位随机
norm := NormalizeCode(code)
if len(norm) != len("JIUKU")+8 {
t.Fatalf("归一化长度应为 %d,得到 %d%q", len("JIUKU")+8, len(norm), norm)
}
// 随机段只含无歧义字母表字符(不含 0/O/1/I/L)
random := parts[1] + parts[2]
for _, r := range random {
if !strings.ContainsRune(codeAlphabet, r) {
t.Fatalf("出现字母表外字符 %q%q", r, code)
}
}
}
}
func TestCodeAlphabetExcludesAmbiguous(t *testing.T) {
for _, bad := range []rune{'0', 'O', '1', 'I', 'L'} {
if strings.ContainsRune(codeAlphabet, bad) {
t.Fatalf("字母表不应包含易混字符 %q", bad)
}
}
}
func TestGenerateRedeemCodeUnique(t *testing.T) {
// 不要求绝对唯一,但大量生成应几乎无碰撞(31^8 空间)
const n = 1000
seen := make(map[string]struct{}, n)
dup := 0
for i := 0; i < n; i++ {
c := NormalizeCode(GenerateRedeemCode())
if _, ok := seen[c]; ok {
dup++
}
seen[c] = struct{}{}
}
if dup > 0 {
t.Fatalf("%d 次生成出现 %d 次重复,随机性可疑", n, dup)
}
}
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# jiu-gencode.sh — 平台方在 EC2 上批量生成兑换券(激活码)的便捷封装。
#
# 自动读取 /opt/jiu/config/production.env 的数据库连接,调用同目录的
# jiu-gencode 二进制写入码池 license_codes。随 server 发版部署到
# /opt/jiu/backend/,无需再本地交叉编译 + scp。
#
# 远程一行用法(在你的 Mac 上):
# ssh -i ~/.ssh/wangjia.pem ec2-user@18.136.60.128 \
# '/opt/jiu/backend/jiu-gencode.sh -type annual -days 365 -count 100 -batch 2026-summer'
# ssh -i ~/.ssh/wangjia.pem ec2-user@18.136.60.128 \
# '/opt/jiu/backend/jiu-gencode.sh -type trial -days 7 -count 1'
#
# 参数透传给 jiu-gencode-type trial|monthly|annual|lifetime -days N(0=永久)
# -devices N(0=不改门店现值) -count N -batch <批次> -note <备注>
set -euo pipefail
ENV_FILE="${JIU_ENV_FILE:-/opt/jiu/config/production.env}"
BIN="$(dirname "$0")/jiu-gencode"
[ -f "$ENV_FILE" ] || { echo "jiu-gencode: 找不到环境文件 $ENV_FILE" >&2; exit 1; }
[ -x "$BIN" ] || { echo "jiu-gencode: 找不到可执行文件 $BIN" >&2; exit 1; }
# 加载 production.env:跳过注释/空行,逐行 export(不回显,避免泄漏密钥)。
while IFS= read -r line; do
case "$line" in \#*|'') continue ;; esac
export "$line"
done < "$ENV_FILE"
exec "$BIN" "$@"
-3
View File
@@ -5,9 +5,6 @@ DATABASE_DSN=root:CHANGE_ME_DB_PASS@tcp(127.0.0.1:3306)/jiu_db?charset=utf8mb4&p
JWT_SECRET=CHANGE_ME_RANDOM_32CHARS
JWT_ACCESS_EXPIRE_MIN=60
JWT_REFRESH_EXPIRE_H=168
LICENSE_HMAC_SECRET=CHANGE_ME_RANDOM_32CHARS
LICENSE_ED25519_PUBLIC_KEY=zFkRV0E+oMjHbixiBamf2ZAq+n7En2V7EJRIurgtVxo=
LICENSE_ED25519_PRIVATE_KEY=CHANGE_ME_ED25519_PRIVATE_FROM_BITWARDEN
STORAGE_UPLOAD_DIR=/opt/jiu/images
STORAGE_BASE_URL=https://jiu.51yanmei.com/images
STORAGE_PUBLIC_URL=https://jiu.51yanmei.com
+1 -1
View File
@@ -16,7 +16,7 @@ set -euo pipefail
# ---- 可配置项 ----
RBW="${RBW_BIN:-/opt/homebrew/bin/rbw}"
BW_ITEM="${JIU_BW_ITEM:-jiu db password}" # 存放 jiu 密钥字段的 Bitwarden 条目
SECRET_KEYS="DATABASE_DSN JWT_SECRET LICENSE_HMAC_SECRET LICENSE_ED25519_PRIVATE_KEY DB_PASSWORD"
SECRET_KEYS="DATABASE_DSN JWT_SECRET DB_PASSWORD"
EC2_HOST="${EC2_HOST:-18.136.60.128}"
EC2_USER="${EC2_USER:-ec2-user}"
+6 -1
View File
@@ -13,10 +13,14 @@ echo "==> compile-backend: tag=${TAG}"
# Build Go backend (linux/amd64 for EC2)
cd backend
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o jiu-server .
# Platform code-minting CLI (gencode) ships alongside the server so codes can be
# minted on EC2 without cross-compiling/scp each time.
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o jiu-gencode ./cmd/gencode
cd ..
mkdir -p dist
mv backend/jiu-server dist/jiu-server
mv backend/jiu-gencode dist/jiu-gencode
# Shared infrastructure (nginx/systemd/env/compose). version.yaml is NOT here —
# it belongs to the client pipeline.
@@ -26,7 +30,8 @@ tar -czf dist/configs.tar.gz \
deploy/production.env.template \
deploy/setup-ec2.sh \
deploy/docker-compose.yml \
deploy/docker-compose.jiu.yml
deploy/docker-compose.jiu.yml \
deploy/jiu-gencode.sh
echo "==> compile-backend: done — dist/ contents:"
ls -lh dist/
+18
View File
@@ -27,6 +27,13 @@ echo "==> deploy-server: uploading files to EC2"
${SCP} dist/jiu-server "${EC2_USER}@${EC2_HOST}:/tmp/jiu-server"
${SCP} /tmp/jiu-configs/deploy/nginx-jiu.conf "${EC2_USER}@${EC2_HOST}:/tmp/nginx-jiu.conf"
# Platform code-minting CLI + wrapper (absent when rolling back to a pre-gencode
# release — upload only when present).
if [ -f dist/jiu-gencode ]; then
${SCP} dist/jiu-gencode "${EC2_USER}@${EC2_HOST}:/tmp/jiu-gencode"
${SCP} /tmp/jiu-configs/deploy/jiu-gencode.sh "${EC2_USER}@${EC2_HOST}:/tmp/jiu-gencode.sh"
fi
${SSH} "${EC2_USER}@${EC2_HOST}" << 'ENDSSH'
set -e
@@ -35,6 +42,17 @@ sudo systemctl stop jiu
cp /tmp/jiu-server /opt/jiu/backend/jiu-server
chmod +x /opt/jiu/backend/jiu-server
# Refresh the code-minting CLI if shipped in this release (skip gracefully on
# rollback to a pre-gencode release). Not a service — just a host-side binary.
if [ -f /tmp/jiu-gencode ]; then
cp /tmp/jiu-gencode /opt/jiu/backend/jiu-gencode
chmod +x /opt/jiu/backend/jiu-gencode
if [ -f /tmp/jiu-gencode.sh ]; then
cp /tmp/jiu-gencode.sh /opt/jiu/backend/jiu-gencode.sh
chmod +x /opt/jiu/backend/jiu-gencode.sh
fi
fi
# Start and health check
sudo systemctl start jiu
echo "Waiting for health check..."
+2 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# release-server.sh <tag> — create the Forgejo Release for the backend pipeline
# and upload jiu-server + configs.tar.gz.
# and upload jiu-server + jiu-gencode + configs.tar.gz.
set -euo pipefail
# shellcheck source=scripts/ci/lib-forgejo.sh
@@ -20,6 +20,7 @@ PYEOF
create_release "$TAG" "$BODY"
upload_asset dist/jiu-server
upload_asset dist/jiu-gencode
upload_asset dist/configs.tar.gz
echo "==> release-server: done — Release ${TAG} created"