Compare commits

...

89 Commits

Author SHA1 Message Date
wangjia b0a74f3607 chore: release v1.0.26
Deploy / build-linux-web (push) Successful in 55s
Deploy / build-windows (push) Successful in 2m2s
Deploy / build-macos (push) Successful in 1m17s
Deploy / build-android (push) Successful in 1m30s
Deploy / build-ios (push) Successful in 6s
Deploy / release-deploy (push) Successful in 1m43s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:40:54 +08:00
wangjia 543bd2712f chore: update todo list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:40:17 +08:00
wangjia fab7bd21dd feat: #13 公开商品 API 补充 code/barcode/batch.quantity,报错反馈携带商品和门店信息
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:39:59 +08:00
wangjia 3b2ebc0202 fix(web): 注册页 API URL 改用 site.appBaseUrl,修正成功提示导航路径
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:33:16 +08:00
wangjia ff32c8a845 fix(client): 公开页页脚「关于岩美」链接改为 AppConfig.aboutUrl 配置项
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 22:27:38 +08:00
wangjia af64e29459 fix(client): 公开页页脚「关于岩美」链接修正为 jiu.51yanmei.com
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 09:19:33 +08:00
wangjia f788a7ab74 chore: release v1.0.25
Deploy / build-windows (push) Successful in 1m56s
Deploy / build-linux-web (push) Failing after 13m56s
Deploy / build-macos (push) Has been skipped
Deploy / build-android (push) Has been skipped
Deploy / build-ios (push) Has been skipped
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 08:20:48 +08:00
wangjia 8a87d4a82c feat(client): #14/#18 产地/保质期/储存方式/描述文档 provider & repo 及公开页意见反馈
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 08:20:00 +08:00
wangjia 0d17533120 feat(client): 基础数据管理新增产地/保质期/储存方式/描述文档 4 个字典维护 Tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 08:06:21 +08:00
wangjia 4e64e5eb47 feat: 入库单/商品详情展示产地保质期储存方式,商品详情显示建议零售价
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 07:57:10 +08:00
wangjia 1ec1d4209a chore: release v1.0.24
Deploy / build-linux-web (push) Successful in 59s
Deploy / build-windows (push) Successful in 1m50s
Deploy / build-macos (push) Successful in 1m22s
Deploy / build-android (push) Successful in 1m26s
Deploy / build-ios (push) Successful in 7s
Deploy / release-deploy (push) Successful in 1m39s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 07:31:55 +08:00
wangjia 22f07ed805 feat(client): 入库表单关联商品属性字典(产地/保质期/储存方式/描述文档)
- product_option.dart: 新增 4 个字典 model(Origin/ShelfLife/Storage/DescriptionDoc)
- product_option_repository.dart: 新增 4 个 list 方法(listOrigins/ShelfLives/Storages/DescriptionDocs)
- product_option_provider.dart: 新增 4 个 FutureProvider 供表单使用
- product_repository.dart: findOrCreate 加 originId/shelfLifeId/storageId/descriptionDocId 可选参数
- stock_in_form_screen.dart: _ItemRow 加 4 个可选 ID 字段;新增 4 个 SearchableOptionField 方法;移动端卡片视图增加 4 个可选字段;提交时传入属性 ID

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 07:03:07 +08:00
wangjia 45b6ec832b feat(client): 公开商品页改读真实数据,移除茅台 mock 硬编码
- _DescriptionSection: 接受 title/body/keywords 三参数,移除 _mockBody/_mockKeywords 和茅台专属文案
- _ParamsCard: 新增 origin/shelfLife/storage 参数;产地空时不显示,生产日期无时不显示,保质期/储存方式用后端返回值(含默认话术)
- 数据提取段:新增 origin/shelfLife/storage/descriptionTitle/descriptionKeywords 从 API 响应读取
- 删除未使用的 _extractKeywords 方法

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 06:58:28 +08:00
wangjia ba127826b2 feat(backend): 新增商品属性字典表(产地/保质期/储存方式/描述文档)
- model/product_attr.go: 新增 4 张字典表 struct(TenantBase 范式)
- model/product.go: Product 增加 4 个可空外键 + belongs-to 关联
- handler/product_attr.go: 4 组 16 个 CRUD handler(shop_id 隔离)
- handler/product.go: FindOrCreate/Update 支持写入 4 个属性 ID
- handler/public.go: Preload 4 表 + 三级回退介绍 + 默认话术常量
- router/router.go: /product-options/{origins,shelf-lives,storages,description-docs} 路由
- main.go: AutoMigrate 注册 4 个新 model
- testutil/setup.go: SQLite 测试库补齐新列与新表

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 06:56:16 +08:00
wangjia 0e2a4086e1 chore(todo): 补录 product 公开页 6 条待办(价格展示、硬编码清理、死交互等) 2026-06-08 02:12:54 +08:00
wangjia 99f34a6223 chore(web): 删除 scan.html,清理 /scan/ nginx 路由和文档
scan.html 从未被实际触发——二维码 URL 格式为 /product/<uuid>,
由 Flutter Web PublicProductScreen 处理,scan.html 是孤立死页。

- 删除 web/scan.html
- 移除 nginx-jiu.conf 中 /scan/ location 块
- 更新文档路由表(/scan/<id> → /product/<uuid>)
- 标记 todo #4 完成

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 02:07:19 +08:00
wangjia ce92d619ed chore(todo): 标记 #1 两套 CSS 设计令牌问题已完成(v1.0.23) 2026-06-08 01:48:41 +08:00
wangjia 22a31f6eb8 fix(web): 合并两套 CSS 设计令牌,清理 features 冗余样式
- 删除 colors_and_type.css(与 color.css 完全相同,改 scan.html 引用 color.css)
- 删除 shared.css(无任何页面引用,已被 base.njk+style.css 替代)
- 移除 features-inventory.css / features-approval.css 头部 105 行重复代码
  (reset、.btn、.container、.section、.eyebrow、.topnav 均由 base.njk 提供,features 页已走模板)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 01:48:32 +08:00
wangjia 1c901432a6 chore(todo): 删除不再需要的旧版备份 todo.legacy.html 2026-06-08 01:40:17 +08:00
wangjia e124ed7c65 feat(todo): 新增 /todo slash command,支持 add/done/list/reopen/rm
- todo/todo.json:结构化 JSON 真相源(id/标题/重要度3级/平台标签/版本)
- todo/todo.mjs:Node 零依赖单文件 CLI,写操作自动重渲染 HTML
- todo/todo.html:重新设计布局(未完成置顶/已完成置底/sticky 筛选栏)
- .claude/commands/todo.md:/todo slash command
- CLAUDE.md:追加 TODO 管理规范(唯一真相源、完成即标记、禁止使用其它 todo 工具)

旧 todo.html 备份为 todo.legacy.html;存量 6 条待办已导入 todo.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 01:31:41 +08:00
wangjia 46304ab961 chore: release v1.0.23
Deploy / build-windows (push) Successful in 3m34s
Deploy / build-macos (push) Has been cancelled
Deploy / build-android (push) Has been cancelled
Deploy / build-ios (push) Has been cancelled
Deploy / release-deploy (push) Has been cancelled
Deploy / build-linux-web (push) Has been cancelled
2026-06-08 01:29:11 +08:00
wangjia d1260e8258 refactor(web): features 页改为 njk 模板,接入 base.njk 统一导航页脚
- inventory.html / approval.html → .njk,使用 layout: base.njk
- 内联 CSS 提取为独立文件 assets/features-{name}.css(pageExtraCss 加载)
- 移除各自硬编码的 nav/footer/script,改由 nav.njk、footer.njk、nav.js 统一提供
- permalink 保持 /features/*.html,现有链接无需修改
- .eleventy.js 移除 features/ passthrough

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 22:05:35 +08:00
wangjia 5b980ec58b fix(web): 同步 features 页导航与 nav.njk 一致(补「库存」「审核流」链接)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 21:47:52 +08:00
wangjia 2fc5776435 chore: release v1.0.22
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-windows (push) Successful in 1m54s
Deploy / build-macos (push) Successful in 1m19s
Deploy / build-android (push) Successful in 1m21s
Deploy / build-ios (push) Successful in 8s
Deploy / release-deploy (push) Successful in 1m42s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 21:24:52 +08:00
wangjia bc252d1fcb chore: 更新 release 命令支持自动版本号,添加 web todo 清单
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 21:22:52 +08:00
wangjia c0001f08c2 fix(web): 官网 todo 清单全量修复(联系方式/死链/按钮/导航/下载)
- 联系方式:site.json email 改为真实地址,移除假电话,页脚条件渲染
- 死链:功能页 /download.html → /download/,页脚 href="#" 全部落地或移除
- 按钮指向:价格「购买」→ /register/,「联系销售/演示」→ mailto
- 导航:新增库存管理、审核流直链;模块卡加「了解更多」入口
- iOS 矛盾:首页平台卡标注「TestFlight 内测中」与下载页对齐
- 下载:API 域名抽到 site.json;接口失败时按钮降级为禁用态
- FAQ:试用版/单店版口径统一,明确「注册即试用 30 天」
- 文档:⌘K/Ctrl+K 绑定聚焦搜索框;反馈按钮改为本地 UI,不调不存在的接口
- dist:清理旧 docs.html 残留

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 21:13:19 +08:00
wangjia 46ef7db480 fix(web): 删除官网未实现功能描述,修正营销文案
- approval.html: 删除红冲/Block 5 整块、移除调拨单引用、删除「允许超量出库」bullet、中性化「录错了不慌」场景
- inventory.html: 删除「库存预警推送/SMS」块、「过期预警」bullet、移除「v1.6.2 新增」标注
- index.njk: Hero 版本号改为动态、信任指标替换为真实数据、移动端功能列表去虚假功能

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:39:39 +08:00
wangjia 0c96d5e325 feat(web): 官网移动端适配
- 导航栏:≤768 隐藏横向链接,显示汉堡按钮;点击展开下拉菜单,含全部链接和 CTA
- 新增 ≤600 手机断点:footer 单列、grid 单列、container 内边距收窄、标题字号下调
- 首页:≤768 隐藏 Hero mock 预览(装饰性),≤600 CTA 按钮纵向排列
- features 页(库存/审批):hamburger 导航、≤768 隐藏 composite 视觉、≤600 footer 单列
- 文档页(docs.css):≤600 表格横向可滚
- 下载页:≤600 平台卡片单列排列

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:22:00 +08:00
wangjia ced407ea87 feat(client): 移动端布局适配 + 信息架构整理
- 入库/出库详情页:窄屏商品明细切换为卡片竖排,顶部操作收入溢出菜单
- 基础数据:新增「仓库」tab,使用 DataTableCard + mobileCards 适配窄屏
- 系统设置:移除仓库 tab(已迁入基础数据),新增「授权」tab,数据导入改名「数据管理」
- 关于我们:移除授权信息,补充帮助与文档/扫码防伪/法律信息/系统信息等模块
- AppInfo 新增 phone/wechat/termsUrl/privacyUrl/docsUrl 字段(可选,空则不渲染)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 20:16:34 +08:00
wangjia e550b73d6d chore(commands): build/test/release 流程统一用 claude-sonnet-4-6 模型,新增 /build /test 命令
- release.md 补 frontmatter(model: claude-sonnet-4-6)
- 新增 /build:go build/vet + flutter analyze 编译静态检查
- 新增 /test:go test + flutter test
- 三者均通过 frontmatter 指定 sonnet 4.6

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:05:56 +08:00
wangjia fa02fdbfd8 chore: release v1.0.21
Deploy / build-linux-web (push) Successful in 54s
Deploy / build-windows (push) Successful in 1m57s
Deploy / build-macos (push) Successful in 1m16s
Deploy / build-android (push) Successful in 1m30s
Deploy / build-ios (push) Successful in 9s
Deploy / release-deploy (push) Successful in 1m43s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:53:04 +08:00
wangjia 211fafc7e1 feat(client): 商品详情页支持拍照添加照片(Android/iOS)
- 移动端加图改用 image_picker,底部弹出「拍照 / 从相册选择」二选一;
  桌面/Web 保持 file_picker 文件选择
- 用 defaultTargetPlatform 判断移动端,避免在参与 Web 编译的文件引入 dart:io
- iOS:Info.plist 增加 NSCameraUsageDescription / NSPhotoLibraryUsageDescription
- Android:拍照走系统相机、相册走 Photo Picker,均无需声明权限

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:47:08 +08:00
wangjia 5278bea940 chore: release v1.0.20
Deploy / build-linux-web (push) Successful in 47s
Deploy / build-macos (push) Successful in 1m14s
Deploy / build-android (push) Successful in 1m12s
Deploy / build-ios (push) Successful in 8s
Deploy / build-windows (push) Failing after 12m32s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:40:43 +08:00
wangjia 0bc3f4b702 fix(client): Android release 包缺少 INTERNET 权限导致登录页「服务不可达」
release 包走 main AndroidManifest,Flutter 模板默认只在 debug/profile 清单
注入 INTERNET 权限,主清单不带;强制 release 签名后联网权限缺失,所有网络
请求失败。在主清单显式声明 android.permission.INTERNET。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:39:23 +08:00
wangjia cfeb8f0bba chore: release v1.0.19
Deploy / build-linux-web (push) Successful in 54s
Deploy / build-windows (push) Successful in 1m47s
Deploy / build-macos (push) Successful in 1m18s
Deploy / build-android (push) Successful in 1m3s
Deploy / build-ios (push) Successful in 8s
Deploy / release-deploy (push) Successful in 1m35s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:50:34 +08:00
wangjia 480ce836bb chore: release v1.0.18
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-windows (push) Successful in 1m48s
Deploy / build-macos (push) Successful in 1m17s
Deploy / build-android (push) Successful in 4m13s
Deploy / build-ios (push) Successful in 9s
Deploy / release-deploy (push) Successful in 1m37s
移动端响应式适配(抽屉导航/列表卡片/弹窗自适应)、Android 正式签名与 APK 发布、
iOS(TestFlight) 工程与 CI、多平台构建流水线、相关文档同步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:55:33 +08:00
wangjia 94f0fb2095 chore: release v1.0.17
Deploy / build-linux-web (push) Successful in 51s
Deploy / build-windows (push) Successful in 1m45s
Deploy / build-macos (push) Successful in 1m4s
Deploy / release-deploy (push) Successful in 3m37s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:35:09 +08:00
wangjia 36e5677b7c feat(client): 反馈 Bug/功能建议 改为直接提交后台(文字+附图)
- feedback_repository + provider:图片上传 /feedback/images、提交 /feedback
- 关于我们的反馈入口改为表单弹窗:多行文字 + 选图上传(缩略图/可删,最多9张),
  提交到后台,不再走 mailto
- 平台/版本随提交上报(web 安全:defaultTargetPlatform,不引 dart:io)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:31:46 +08:00
wangjia 7a1d8465e5 feat(backend): 意见反馈接口(提交+图片上传+admin列表)
- model/feedback.go: Feedback(多租户 shop_id,images 以 JSON 存)+ StringSlice 类型
- handler/feedback.go: Submit(认证,shop/user 从 JWT,去规范化 username/shop_code)、
  UploadImage(复用 product_image 压缩逻辑,存 /images/feedback/<shop>/)、
  List(SuperAdminOnly,分页过滤)、UpdateStatus(new/handled)
- router 注册 /feedback、/feedback/images、/admin/feedback;main AutoMigrate;schema.sql 加表
- feedback_test 覆盖提交/校验/权限/列表

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:31:46 +08:00
wangjia dd09f1a526 feat(client): 关于→关于我们,移到左侧菜单独立页面
- 新增 screens/about/about_screen.dart(版本信息/授权信息/关于我们/意见反馈)
- 左侧导航新增「关于我们」(/about),从系统设置移除原「关于」Tab(7→6)
- 迁移并清理 settings 中仅关于页使用的 widget/弹窗/import

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:05:12 +08:00
wangjia 53a52cc530 feat(client): 打印任务默认名 类型_时间戳(标签/入库单/出库单)
Printing.layoutPdf 传 name:标签_yyyyMMdd_HHmmss / 入库单_... / 出库单_...,
作为打印任务名与「打印到 PDF」的默认文件名。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 20:05:12 +08:00
wangjia 337a358d54 docs(changelog): 1.0.16(应用内更新 + 安装程序中文化 + 更新提示优化)
Deploy / build-linux-web (push) Successful in 49s
Deploy / build-macos (push) Successful in 58s
Deploy / build-windows (push) Successful in 1m49s
Deploy / release-deploy (push) Successful in 1m21s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:26:31 +08:00
wangjia c98a90472b feat(client): 更新提示加引导文案,强制更新弹窗文案统一
- banner: 发现新版本 vX,建议立即更新以获得最新功能与修复
- 强制更新弹窗: 去掉 release notes,统一为「需更新后才能继续使用,
  更新以获得最新功能与修复」

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:25:43 +08:00
wangjia 207a9a8343 feat(client): 更新 banner 去掉更新内容 + 安装程序中文化
- 顶部更新 banner 只显示「发现新版本 vX」,不再附带 release notes
- Inno Setup 安装向导改中文:随仓库带 ChineseSimplified.isl(加 UTF-8 BOM),
  .iss 用相对路径引用,compile-windows.sh 把它和 .iss 一起拷到构建目录,
  不依赖 runner 上 Inno Setup 是否自带简中
- (检查更新无更新时的提示已在上个提交加好)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:20:50 +08:00
wangjia d8c0bd74d2 feat(client): 应用内更新(Windows/macOS 下载安装,Web 刷新)
- 新增 lib/core/update/:app_updater(入口+进度对话框) + io/web 条件实现
  - Windows: Dio 下载 setup.exe(无 MOTW) -> 启动安装包 -> 退出,规避浏览器
    SmartScreen 下载页与运行时拦截
  - macOS: 下载 zip -> ditto 解压 -> 后台脚本等退出后替换 /Applications 内
    app 并重启(权限失败回退访达高亮)
  - Web: 刷新页面;其它平台回退浏览器下载
- app_shell 更新 banner/强制更新弹窗、设置页「立即更新」改走应用内更新
- 设置页「检查更新」加反馈(正在检查/已是最新/发现新版本/失败)
- jiu-installer.iss 加 CloseApplications=yes 便于覆盖

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:04:26 +08:00
wangjia 056ae914ce fix(client): Windows main.cpp 用 ASCII 十六进制转义写标题,修复 C4819
Deploy / build-linux-web (push) Successful in 52s
Deploy / build-windows (push) Successful in 1m38s
Deploy / build-macos (push) Successful in 1m3s
Deploy / release-deploy (push) Successful in 1m25s
v1.0.14 Windows 构建失败:main.cpp 含中文(u8 标题+中文注释),MSVC 在
GBK(936) 代码页报 C4819「字符无法表示」,而 runner 开了 /WX 警告即错误。
改为标题用 UTF-8 字节的 \x 十六进制转义、注释改英文,源码纯 ASCII,
运行时 MultiByteToWideChar(CP_UTF8) 解码,窗口标题仍是岩美酒库。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:32:48 +08:00
wangjia 022443476c docs(changelog): 1.0.14(Windows 打印修复 + 关于我们真实信息/配置化)
Deploy / build-linux-web (push) Successful in 50s
Deploy / build-windows (push) Failing after 1m29s
Deploy / build-macos (push) Successful in 1m1s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:48:27 +08:00
wangjia 93f4f87e44 fix(client): 打印字体改 TrueType,修复 Windows 打印卡死
打印字体实为 OpenType/CFF,dart_pdf 只支持 TrueType(glyf),渲染中文
抛 'does not support Unicode';标签打印时异常抛在原生 onLayout 回调内
导致打印插件死等->卡死。用 fonttools 将字体转为 TrueType/glyf 替换。
加回归测试防止退回 CFF 字体。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 01:08:45 +08:00
wangjia 20e7d07be3 feat(client): 关于我们内容改用配置文件 + 窗口标题改岩美酒库
- 新增 assets/config/app_info.json(服务商/网站/邮箱真实值),启动时
  AppInfo.load() 读取,不硬编码、不走环境变量
- 关于我们/续费/反馈处的服务商、网站、邮箱统一读配置
- Windows 窗口标题 jiu_client -> 岩美酒库(u8 + UTF-8→UTF-16 防乱码)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 01:08:45 +08:00
wangjia 1834740676 fix(ci): Inno Setup 去掉中文语言段,用内置英文向导
Deploy / build-windows (push) Successful in 1m37s
Deploy / build-linux-web (push) Successful in 46s
Deploy / build-macos (push) Successful in 1m0s
Deploy / release-deploy (push) Successful in 1m21s
ChineseSimplified.isl 默认不随 Inno Setup 安装,引用它导致编译报
找不到文件。去掉 [Languages] 段用内置默认(英文)向导;app 名称与
快捷方式仍为中文。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:34:44 +08:00
wangjia 90a194fc00 fix(ci): ISCC 调用关闭 MSYS 参数路径转换,修复 more than one script filename
Deploy / build-linux-web (push) Successful in 50s
Deploy / build-windows (push) Failing after 1m22s
Deploy / build-macos (push) Successful in 59s
Deploy / release-deploy (push) Has been skipped
Git Bash(MSYS) 把 /D 开头的参数当 POSIX 路径自动转换,导致 ISCC 的
/DAppVer /DSrcDir /DOutDir 丢失 /D 前缀、被当成多个脚本文件名而报错。
调用 ISCC 时设 MSYS_NO_PATHCONV=1 + MSYS2_ARG_CONV_EXCL='*' 关闭转换。
install-innosetup.ps1 顺带加 /LOG 失败时打印安装日志。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:05:09 +08:00
wangjia fba8b0a917 fix(ci): 健壮化 Inno Setup 安装 + 部署仅保留最新安装包
Deploy / build-linux-web (push) Successful in 51s
Deploy / build-macos (push) Successful in 57s
Deploy / build-windows (push) Failing after 1m22s
Deploy / release-deploy (push) Has been skipped
- 新增 install-innosetup.ps1:校验下载大小、检查安装程序退出码、双
  Program Files 目录定位 ISCC,修复原内联命令静默成功但没真正装上的问题
  (SYSTEM 账户下 Invoke-WebRequest 无 -UseBasicParsing + 未检查退出码)
- provision/compile-windows.sh 在两个 Program Files 目录查找 ISCC
- deploy.sh 发布安装包到 /opt/jiu/downloads 前先清旧包,仅保留最新版本

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 08:42:48 +08:00
wangjia c001f0e8e6 feat(client): Windows 提供 Inno Setup 安装程序 + 下载改同源 HTTPS
Deploy / build-windows (push) Failing after 22s
Deploy / build-linux-web (push) Failing after 14m4s
Deploy / build-macos (push) Has been skipped
Deploy / release-deploy (push) Has been skipped
- Windows 客户端打成 jiu-windows-x64-setup.exe(Inno Setup,中文向导、
  桌面/开始菜单快捷方式、卸载项),替代解压版 zip
- provision-windows.sh 自动静默安装 Inno Setup(缺则装)
- 下载 URL 从内网 HTTP Forgejo 改为 https://jiu.51yanmei.com/downloads/,
  修复 HTTPS 页面下发 http 内网地址被浏览器拦截(无法安全下载),win/mac 均生效
- deploy.sh 将安装包发布到 EC2 /opt/jiu/downloads/,nginx 新增 /downloads/ 路由

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:46:41 +08:00
wangjia f039bd0c61 fix(ci): deploy 适配容器化 nginx(pangolin-edge)
Deploy / build-windows (push) Failing after 25s
Deploy / build-linux-web (push) Successful in 1m14s
Deploy / build-macos (push) Successful in 1m12s
Deploy / release-deploy (push) Has been skipped
nginx 已从主机迁入 pangolin-edge 容器(host 网络模式),其
/etc/nginx/conf.d 由主机 ~/pangolin/edge/conf.d bind-mount。
deploy 改为:jiu.conf 写入该挂载目录 + docker exec pangolin-edge
nginx -t/-s reload,不再 cp 到主机 /etc/nginx 或 systemctl 操作
已废弃的主机 nginx。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:20:50 +08:00
wangjia f4beb90769 fix(ci): nginx reload 改用 systemctl reload-or-restart,避免空 PID 文件报错
Deploy / build-linux-web (push) Successful in 1m1s
Deploy / build-windows (push) Successful in 1m42s
Deploy / build-macos (push) Successful in 1m2s
Deploy / release-deploy (push) Failing after 1m17s
nginx -s reload 依赖 /run/nginx.pid,该文件为空时报 invalid PID number。
改用 systemctl reload-or-restart(经 systemd MAINPID 发 HUP,运行中则 reload、
未运行则启动),并 enable nginx 保证重启后自启。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:33:24 +08:00
wangjia 6f0b273950 fix(ci): Windows 构建清除 cp 复制的插件软链,避免 PathExists(errno 183)
Deploy / build-windows (push) Successful in 1m31s
Deploy / build-linux-web (push) Successful in 55s
Deploy / build-macos (push) Successful in 58s
Deploy / release-deploy (push) Failing after 1m20s
cp -r 把 windows/flutter/ephemeral/.plugin_symlinks 复制成真实目录,
flutter createPluginSymlinks 只清软链、清不掉真目录,重建时撞车。
复制后删除 ephemeral/.dart_tool/build,由 flutter 干净重建。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:06:36 +08:00
wangjia b43601b4fd ci: 运行环境自愈预置 + 发版任务串行 + 删 ci.yml/暂停备份
Deploy / build-windows (push) Failing after 25s
Deploy / build-macos (push) Has been cancelled
Deploy / release-deploy (push) Has been cancelled
Deploy / build-linux-web (push) Has been cancelled
- 新增 scripts/ci/provision-{mac,windows}.sh:幂等预置,按 Flutter 版本
  stamp 短路,pipeline 首跑自动 precache + 预热缓存,运行时不再下载 engine
- scripts/ci/_env.sh:抽出共用镜像 env,compile 脚本统一 source
- deploy.yml:build-macos needs build-linux-web,同一 mac runner 串行
  执行,避免排队超时;各 build job 加 Provision 前置步骤
- 删除 .gitea/workflows/ci.yml
- backup.yml:暂停定时备份(保留 workflow_dispatch 手动触发)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 01:04:23 +08:00
wangjia 3aa63d6575 fix(ci): Windows 构建改用 C:\jiu-build 短路径,弃用 subst W: 映射
Deploy / build-windows (push) Successful in 1m41s
Deploy / build-linux-web (push) Failing after 14m36s
Deploy / build-macos (push) Successful in 33m24s
Deploy / release-deploy (push) Has been skipped
subst W: 因 cmd/MSYS 引号转义失败(报"找不到路径"),导致 W: 盘
未创建、cd /w/client 失败。改为将 client/ 复制到 System32 之外的
C:\jiu-build 构建,规避 WOW64 重定向,并用 PowerShell Compress-Archive 打包。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 00:23:09 +08:00
wangjia a3f9f091b4 fix(ci): Windows 构建用 subst W: 绕过 System32 WOW64 路径重定向
Deploy / build-linux-web (push) Successful in 1m6s
Deploy / build-windows (push) Failing after 16s
Deploy / release-deploy (push) Has been cancelled
Deploy / build-macos (push) Has been cancelled
2026-06-01 01:19:02 +08:00
wangjia b3f5a0f083 debug(ci): 添加 Windows 构建目录诊断输出
Deploy / build-windows (push) Failing after 30s
Deploy / build-linux-web (push) Successful in 1m2s
Deploy / build-macos (push) Failing after 14m5s
Deploy / release-deploy (push) Has been skipped
2026-06-01 01:13:48 +08:00
wangjia 0af92e1732 fix(ci): Windows 构建前重新生成 windows/ 平台目录,避免 CMake 路径不存在
Deploy / build-windows (push) Failing after 28s
Deploy / build-linux-web (push) Successful in 1m11s
Deploy / build-macos (push) Failing after 10m10s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 00:11:47 +08:00
wangjia ea5526944b feat(client): 添加 Flutter Windows 桌面平台支持
Deploy / build-macos (push) Successful in 52s
Deploy / build-windows (push) Failing after 4m27s
Deploy / release-deploy (push) Has been cancelled
Deploy / build-linux-web (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 08:44:31 +08:00
wangjia c2f8bda4f3 chore(ci): 移除 Windows pipeline 中的 Git 安装步骤
Deploy / build-linux-web (push) Successful in 1m8s
Deploy / build-macos (push) Successful in 50s
Deploy / build-windows (push) Failing after 38s
Deploy / release-deploy (push) Has been skipped
Git 已在 Windows runner 上手动安装并加入系统 PATH,无需每次 CI 安装。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 00:17:43 +08:00
wangjia 7f6584ab64 fix(ci): 改从 TUNA 镜像安装 Git,避免访问 GitHub
Deploy / build-windows (push) Failing after 17s
Deploy / build-linux-web (push) Successful in 54s
Deploy / build-macos (push) Successful in 50s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 00:07:47 +08:00
wangjia 23de243598 fix(ci): Windows 安装 Git 改用 cmd,避免 PowerShell 执行策略限制
Deploy / build-windows (push) Failing after 18s
Deploy / build-macos (push) Successful in 56s
Deploy / build-linux-web (push) Failing after 12m28s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 00:01:49 +08:00
wangjia 65e2021138 fix: 修复打印函数缺少 try 块导致 macOS 编译失败;CI 自动安装 Git bash
Deploy / build-windows (push) Failing after 14s
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-macos (push) Successful in 52s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:58:05 +08:00
wangjia d94c6b109b chore: release v1.0.4
Deploy / build-windows (push) Failing after 19s
Deploy / build-linux-web (push) Successful in 51s
Deploy / build-macos (push) Failing after 32s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 23:53:51 +08:00
wangjia 75e3b934bc chore: release v1.0.3
Deploy / build-windows (push) Failing after 9s
Deploy / build-linux-web (push) Successful in 51s
Deploy / release-deploy (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:39:04 +08:00
wangjia ec7596e272 fix(deploy): version.yaml 路径改为 /opt/jiu/config/(与 WorkingDirectory 一致) 2026-05-26 21:34:14 +08:00
wangjia 9fc379f33a chore: release v1.0.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:27:24 +08:00
wangjia ee826eca7f fix(print): 商品标签改为竖向 20mm×38mm 布局
原设计 4in×2in 横向与实际标签纸不符,导致内容旋转 90°。按实测尺寸重设为竖向 20mm×38mm,重新排布:酒行名顶部深色背景→商品名→QR码→规格/批次/生产日期→联系方式底部。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:20:31 +08:00
wangjia 51863dcbd3 feat(deploy): 新增 Windows 桌面客户端构建与发布支持
CI 拆成三个 job(mac 构建 Linux/Web、windows 构建 Windows 桌面、mac 发布部署);新增 compile-windows.sh 用 Flutter 构建并打包为 jiu-windows-x64.zip;release.sh 自动写入 Windows 下载 URL 到 version.yaml 并上传 zip。下载页 Windows 卡片从"敬请期待"改为真实下载按钮,URL 由 API 动态填充。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 18:29:17 +08:00
wangjia eda7d37b64 feat: 新增用户自助注册功能
后端新增 POST /api/v1/public/register 接口,支持门店自助注册并自动生成门店编码(S000001 格式);shops 表加 description 字段。营销站新增 /register/ 注册页,含门店信息和管理员账号表单,注册成功后展示门店编码和登录提示。Flutter 登录页底部加「前往官网注册」跳转链接。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 13:56:34 +08:00
wangjia 471b980179 refactor(web): 营销站重构为 Eleventy SSG,内容迁移至 Markdown
- 引入 Eleventy 静态站点生成器,页面统一使用 Nunjucks 模板
- base.njk 提供公共 nav / footer / CSS,消除三个页面间的重复代码
- 抽取 color.css(设计 token)+ style.css(公共组件 + utility 类)
- index.njk:首页迁移,用 utility class 替换大量页面私有 CSS
- download.njk:下载页重构,版本号/更新日志从 CHANGELOG.md 构建时解析,OS 自动识别
- docs.njk:文档页重构,12 章内容从 web/content/docs.md 构建时渲染,三列布局保留
- docs.css 单独提取,docs 专有样式不污染公共 CSS
- 内容面向非技术用户重写,去除技术术语和内部路径引用

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:24:14 +08:00
wangjia 3d1fc0d4ba fix(client): 统一打印错误处理,失败时上报并弹提示
新增 safePrint 封装,所有打印调用点统一 catch 任意异常(含字体加载失败),
自动上报 error_reporter 并展示 SnackBar,不再静默失败。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:55:56 +08:00
wangjia b6223b3816 chore: release v1.0.2
Deploy / deploy (push) Successful in 1m13s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:25:05 +08:00
wangjia 117cec431b docs(claude): 补充发版流程、异常上报、拼音搜索规范
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 22:09:21 +08:00
wangjia d8295d14da chore: 添加 /release 本地发版 slash command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:52:54 +08:00
wangjia 5282eb57c9 fix(ci): 对 Forgejo API 的 curl 加 -k,跳过自签名证书验证
Deploy / deploy (push) Successful in 1m36s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:47:15 +08:00
wangjia f59bdf056f fix(ci): 去掉 curl -s,显示完整错误输出;修复 CHANGELOG ### 子标题解析
Deploy / deploy (push) Failing after 43s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:41:15 +08:00
wangjia 01946282ed chore: 更新 CHANGELOG v1.0.1
Deploy / deploy (push) Failing after 44s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:31:56 +08:00
wangjia 0d4c9cced2 fix(inventory): 补回 dart:async import(Timer 仍被导入进度弹框使用)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:29:24 +08:00
wangjia e9a6543a8b feat(search): 库存搜索支持拼音/首字母,按回车触发
- 新增 util.ToPinyin(),使用 go-pinyin 生成全拼和首字母
- Product model 新增 name_pinyin / name_initials 列(AutoMigrate)
- 启动时自动回填存量商品拼音
- Create / Update / FindOrCreate 写入时同步生成拼音
- 库存搜索 SQL 加入拼音/首字母 LIKE 条件
- 前端去掉 300ms 防抖,改为回车/点击搜索按钮触发

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:27:54 +08:00
wangjia 5f2248001d feat(client): Dio 拦截器统一上报 5xx 错误,新增 api_error 类型
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:09:50 +08:00
wangjia 2dbc89b902 feat: 错误自动上报机制
- 后端:error_reports 表 + POST /api/v1/public/errors + GET /api/v1/admin/errors
- Flutter:ErrorReporter 单例,60s 去重,fire-and-forget,不影响用户
- main.dart:FlutterError 和 Zone 错误自动上报
- 打印异常:catch 块加 reportError,Windows 打印问题可自动采集堆栈

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:08:03 +08:00
wangjia 44ffedb56a fix(client): 打印异常加 try-catch,错误以 SnackBar 提示用户
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:41:15 +08:00
wangjia 93878511e5 docs: 补充 SSH remote 地址
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:51:12 +08:00
wangjia 503ee7372e chore: git remote 迁移至 git.51yanmei.com,新增 Android 平台支持
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:49:22 +08:00
wangjia 62dc54fe1c docs: 补充开发完成标准(DoD),明确测试通过前不得提交
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:37:34 +08:00
234 changed files with 20508 additions and 9576 deletions
+35
View File
@@ -0,0 +1,35 @@
---
description: 本地编译与静态检查(后端 go build/vet + 前端 flutter analyze
argument-hint: 可选 backend | client,留空则全部
model: claude-sonnet-4-6
allowed-tools: Bash
---
执行本地编译与静态检查,用于快速验证代码可编译、无警告。**不产出发布制品**(多平台打包由 CI 在发版时完成)。
参数 `$ARGUMENTS`
- 留空:检查后端 + 前端
- `backend`:仅检查后端
- `client`:仅检查前端
任意步骤失败立即停止并报告错误(贴出关键错误输出)。
## 后端检查(参数为空或 backend 时执行)
```bash
export PATH="/opt/homebrew/bin:$PATH"
cd backend && go build ./... # 1. 编译通过
cd backend && go vet ./... # 2. 无编译警告
```
## 前端检查(参数为空或 client 时执行)
```bash
export PATH="/opt/homebrew/bin:$PATH"
cd client && flutter analyze --no-fatal-infos --no-fatal-warnings # error 才算失败,warning/info 允许
```
## 完成报告
全部通过后,用一行 `result:` 汇总(如 `result: 后端 build+vet 通过、前端 analyze 无 error`)。
任一失败则停止,指出失败的命令和原因。
+89
View File
@@ -0,0 +1,89 @@
---
description: 执行本地发版流程(build → test → CHANGELOG → commit → tag → push
argument-hint: <version> 如 1.0.22
model: claude-sonnet-4-6
allowed-tools: Bash, Read, Edit, Write
---
执行本地发版流程。
**确定版本号:**
- 如果 `$ARGUMENTS` 非空,使用它作为版本号(VERSION)。
- 如果 `$ARGUMENTS` 为空,运行 `git describe --tags --abbrev=0` 获取最新 tag(如 `v1.0.20`),去掉 `v` 前缀后将 patch 号加 1(如 `1.0.21`),作为版本号(VERSION)。将自动确定的版本号告知用户。
后续所有步骤中,将确定好的版本号记为 VERSION。
按以下步骤顺序执行,任意步骤失败立即停止并报告错误。
## 1. 检查工作区
检查是否有未提交的改动(`git status`)。如果有,列出文件并询问用户是否继续。
## 2. 本地构建
```bash
export PATH="/opt/homebrew/bin:$PATH"
cd backend && go build ./...
```
## 3. 运行测试
```bash
export PATH="/opt/homebrew/bin:$PATH"
cd backend && go test ./...
cd client && flutter analyze --no-fatal-infos --no-fatal-warnings
```
## 4. 更新 CHANGELOG.md
检查 CHANGELOG.md 中是否已有 `## [VERSION]` 版本节。
- **已有**:直接使用,不修改。
- **没有**:读取 `git log` 从上一个 tag 到 HEAD 的提交记录,自动生成一个版本节插入到文件顶部(位于已有版本节之前),格式如下:
```
## [VERSION] - <今天日期>
### 新功能
- ...
### 改进
- ...
### 修复
- ...
```
只保留有内容的分类(新功能/改进/修复),根据 commit message 的类型(feat/fix/refactor 等)归类。
**CHANGELOG 撰写原则:**
- 站在**用户视角**总结,描述对使用体验的实际影响,而非技术实现
- **忽略**以下类型的提交(产品感知弱):
- 发版工具、CI/CD、部署脚本相关(chore: release、ci:、devops:
- 内部文档、开发规范、CLAUDE.md 更新(docs(claude):、docs(internal):
- 代码格式、lint、重构(style:、refactor: 纯内部)
- 依赖升级、版本号 bumpchore: bump、chore: upgrade
- **保留并重点描述**:feat、fix、perf 类,以及用户能感知到的 refactor(如界面改版、交互优化)
- 每条描述用一句话说清楚"用户能做什么"或"修了什么问题",不堆砌技术术语
## 5. 提交代码
将所有改动(包括 CHANGELOG.md)用以下格式提交到 main
```
chore: release vVERSION
```
## 6. 打 tag
```bash
git tag vVERSION
```
## 7. 推送
```bash
git push ssh://git@git.51yanmei.com:2222/wangjia/jiu.git main vVERSION
```
推送成功后提示用户:CI/CD 已触发,等待 Telegram 通知。
+36
View File
@@ -0,0 +1,36 @@
---
description: 运行本地测试(后端 go test + 前端 flutter test
argument-hint: 可选 backend | client,留空则全部
model: claude-sonnet-4-6
allowed-tools: Bash
---
运行本地自动化测试。任意用例失败立即停止并报告(贴出失败用例与关键输出)。
参数 `$ARGUMENTS`
- 留空:后端 + 前端
- `backend`:仅后端
- `client`:仅前端
## 后端测试(参数为空或 backend 时执行)
```bash
export PATH="/opt/homebrew/bin:$PATH"
cd backend && go test ./...
```
不得有 FAIL。`[no test files]` 属正常(该包无测试)。
## 前端测试(参数为空或 client 时执行)
```bash
export PATH="/opt/homebrew/bin:$PATH"
cd client && flutter test
```
`All tests passed!`
## 完成报告
全部通过后用一行 `result:` 汇总(如 `result: 后端 go test 全过、前端 121 测试全过`)。
任一失败则停止,列出失败用例名与原因,不要自行修改业务代码(如需修复,告知用户或交给对应 coder)。
+67
View File
@@ -0,0 +1,67 @@
---
description: 项目 TODO 管理(list / add / done / reopen / rm
argument-hint: "list | add <描述> | done <id> [version] | reopen <id> | rm <id>"
model: claude-sonnet-4-6
allowed-tools: Bash, Read
---
管理项目 `todo/todo.json` 待办清单,所有写操作会自动重渲染 `todo/todo.html`
**重要规则**:所有待办项必须通过本命令管理,禁止使用其它 todo 工具(TaskCreate/TodoWrite 等)。
---
根据 `$ARGUMENTS` 分派,缺省等同 `list`
## list(或无参数)
运行以下命令并将终端 summary 转述给用户,同时提示 HTML 路径便于浏览:
```bash
node todo/todo.mjs list
```
## add <描述>
`$ARGUMENTS` 去掉首词 `add` 后的内容解析为待办描述,推断:
- `--title`:核心一句话标题(简洁,去掉平台/类别信息)
- `--level``high`(阻断/紧急)/ `mid`(重要,默认)/ `low`(一般/优化)
- 关键词「紧急」「阻断」「必须」「严重」→ high;「重要」「需要」→ mid;其余 → low
- `--tags`:从描述推断平台/类别,可多个逗号分隔,常用值:`前端,后端,Web,mac,Windows,Android,iOS,数据库,CI/CD,文档`
- `--desc`:补充说明(可选,如有具体文件/路径/上下文则填入)
然后运行:
```bash
node todo/todo.mjs add --title "..." --level mid --tags "前端,Web" --desc "..."
```
转述:「已添加 #id [级别] 标题 标签」,并显示 summary。
## done <id> [version]
`$ARGUMENTS` 提取 id(数字)和可选 version(格式 vX.Y.Z):
- 有 version`node todo/todo.mjs done <id> --version <version>`
- 无 version`node todo/todo.mjs done <id>`(脚本自动用 `git describe --tags --abbrev=0`
转述:「#id 标记完成,记入版本 vX.Y.Z」,并显示 summary。
## reopen <id>
```bash
node todo/todo.mjs reopen <id>
```
转述「#id 已重新开启」。
## rm <id>
```bash
node todo/todo.mjs rm <id>
```
转述「#id 已删除」。
---
所有命令完成后,在终端显示来自脚本的完整 summary(共/未完成/已完成及分级列表)。
+3 -2
View File
@@ -1,8 +1,9 @@
name: DB Backup
on:
schedule:
- cron: '0 18 * * *' # UTC 18:00 = 北京时间 02:00
# 定时备份已暂停(保留手动触发)。恢复时取消下面 schedule 的注释即可。
# schedule:
# - cron: '0 18 * * *' # UTC 18:00 = 北京时间 02:00
workflow_dispatch:
jobs:
-30
View File
@@ -1,30 +0,0 @@
name: CI
on:
push:
branches-ignore: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum
- name: Go tests
working-directory: backend
run: go test ./...
- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true
- name: Flutter tests
working-directory: client
run: flutter test
+152 -2
View File
@@ -6,7 +6,7 @@ on:
- 'v[0-9]*.[0-9]*.[0-9]*'
jobs:
deploy:
build-linux-web:
runs-on: mac
env:
GOPROXY: https://goproxy.cn,direct
@@ -16,9 +16,159 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Compile
- name: Provision (idempotent — auto-runs if host not initialized)
run: sh scripts/ci/provision-mac.sh
- name: Compile (Linux backend + Flutter Web)
run: sh scripts/ci/compile.sh "${{ gitea.ref_name }}"
- name: Upload linux-web artifacts
uses: actions/upload-artifact@v3
with:
name: linux-web
path: dist/
build-macos:
# Same physical runner as build-linux-web (label: mac, capacity 1). Run them
# serially via needs so the second job doesn't sit queued and time out.
needs: build-linux-web
runs-on: mac
env:
PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
steps:
- uses: actions/checkout@v4
- name: Provision (idempotent — auto-runs if host not initialized)
run: sh scripts/ci/provision-mac.sh
- name: Compile (Flutter macOS)
run: sh scripts/ci/compile-macos.sh "${{ gitea.ref_name }}"
- name: Upload macos artifacts
uses: actions/upload-artifact@v3
with:
name: macos
path: dist/
build-android:
# Same physical mac runner (label: mac, capacity 1) — it already has the
# Android SDK. Serialize after build-macos via needs so it doesn't sit
# queued and time out.
needs: build-macos
runs-on: mac
env:
PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
steps:
- uses: actions/checkout@v4
- name: Provision (idempotent — auto-runs if host not initialized)
run: sh scripts/ci/provision-mac.sh
- name: Compile (Flutter Android APK)
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: sh scripts/ci/compile-android.sh "${{ gitea.ref_name }}"
- name: Upload android artifacts
uses: actions/upload-artifact@v3
with:
name: android
path: dist/
build-ios:
# Same physical mac runner (label: mac, capacity 1) with Xcode. Serialize
# after build-android. Builds a signed IPA and uploads to TestFlight; if the
# iOS signing secrets are absent the script skips gracefully (exit 0), so this
# job is safe to keep before the Apple Developer account is configured.
needs: build-android
runs-on: mac
env:
PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
steps:
- uses: actions/checkout@v4
- name: Provision (idempotent — auto-runs if host not initialized)
run: sh scripts/ci/provision-mac.sh
- name: Compile & upload to TestFlight (Flutter iOS)
env:
IOS_DIST_CERT_P12_BASE64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }}
IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
IOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
APPSTORE_API_KEY_P8_BASE64: ${{ secrets.APPSTORE_API_KEY_P8_BASE64 }}
run: sh scripts/ci/compile-ios.sh "${{ gitea.ref_name }}"
build-windows:
runs-on: windows
env:
GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
steps:
- uses: actions/checkout@v4
- name: Provision (idempotent — auto-runs if host not initialized)
shell: bash
run: sh scripts/ci/provision-windows.sh
- name: Compile (Flutter Windows)
shell: bash
run: sh scripts/ci/compile-windows.sh "${{ gitea.ref_name }}"
- name: Upload windows artifacts
uses: actions/upload-artifact@v3
with:
name: windows
path: dist/
release-deploy:
needs: [build-linux-web, build-macos, build-android, build-ios, build-windows]
runs-on: mac
env:
GOPROXY: https://goproxy.cn,direct
PUB_HOSTED_URL: https://pub.flutter-io.cn
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
steps:
- uses: actions/checkout@v4
- name: Download linux-web artifacts
uses: actions/download-artifact@v3
with:
name: linux-web
path: dist/
- name: Download macos artifacts
uses: actions/download-artifact@v3
with:
name: macos
path: dist/
- name: Download android artifacts
uses: actions/download-artifact@v3
with:
name: android
path: dist/
- name: Download windows artifacts
uses: actions/download-artifact@v3
with:
name: windows
path: dist/
- name: Test
run: sh scripts/ci/test.sh
+7
View File
@@ -23,6 +23,13 @@ client/devtools_options.yaml
# Flutter 构建产物
client/build/
# CI 构建产物
dist/
# 营销站构建产物
web/dist/
web/node_modules/
client/.dart_tool/
client/.flutter-plugins
client/.flutter-plugins-dependencies
+174
View File
@@ -5,6 +5,180 @@ 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.26] - 2026-06-08
### 新功能
- 公开商品 API 新增商品编码、条码、当前库存数量字段,供扫码页及第三方集成使用
- 公开页「商品报错」「意见反馈」提交时自动携带门店编号和商品信息,方便运营追踪
### 修复
- 注册页 API 地址改为配置文件驱动,支持分离部署场景(不再依赖 window.location.origin
- 注册成功提示的客户端导航路径由「系统设置 → 关于」修正为「系统设置」
- 公开页页脚「关于岩美」链接改为配置项,不再硬编码
## [1.0.25] - 2026-06-08
### 新功能
- 基础数据页新增「产地」「保质期」「储存方式」「描述文档」4 个字典维护 Tab,可直接在基础数据中增删改查
- 商品详情页展示产地、保质期、储存方式参数,并在介绍区上方显示建议零售价
- 入库单详情中商品列展示产地、保质期、储存方式信息
- 公开扫码页新增「商品报错」「意见反馈」功能,用户可提交反馈
## [1.0.24] - 2026-06-08
### 新功能
- 扫码商品页新增「查看本店其他商品」功能,点击即可浏览该门店全部上架商品
- 门店可在设置中填写微信号,顾客扫码后可直接查看并添加门店微信
- 商品公开页展示建议零售价(有填写时显示)
- 入库时可为商品关联产地、保质期、储存方式、描述文档,关联后扫码页自动呈现真实信息
- 扫码商品页产地、保质期、储存方式、商品介绍改读数据库真实数据,不再显示占位内容
### 改进
- 新增产地/保质期/储存方式/描述文档四类基础数据字典,支持增删改查,可在入库时按需关联商品
## [1.0.23] - 2026-06-07
### 修复
- 官网功能详情页(库存管理/审核流)导航栏现与主站完全一致,不再缺少「库存」「审核流」入口
### 改进
- 官网功能详情页接入统一模板系统,导航/页脚/版权信息改一处自动同步全站
## [1.0.22] - 2026-06-07
### 新功能
- 官网移动端适配:手机浏览器下导航折叠、首页各区块单列排版、features 详情页和页脚自适应窄屏
### 改进
- 客户端移动端布局优化:入库/出库详情页商品明细改为卡片式竖排,操作按钮收纳至溢出菜单,窄屏不再横向溢出
- 信息架构整理:仓库管理移入基础数据,系统设置新增授权 Tab 和数据管理组,关于我们补充帮助文档/法律信息/系统信息入口
### 修复
- 官网所有功能页死链修正(`/download.html``/download/``/docs.html``/docs/`
- 官网删除未实现功能的虚假宣传(红冲、调拨单、库存推送通知、过期预警等),文案与实际功能对齐
- 官网客服邮箱更新为真实邮箱,移除假冒客服电话,页脚清除无目标死链
- 导航新增「库存」「审核流」功能页入口,首页模块卡加「了解更多」链接
- 下载页接口失败时按钮改为禁用态并提示,不再无响应
- 首页 iOS 平台说明改为「TestFlight 内测中」,与下载页状态一致
## [1.0.21] - 2026-06-07
### 新功能
- 商品详情页添加照片时,手机端(Android/iOS)支持直接拍照或从相册选择
## [1.0.20] - 2026-06-07
### 修复
- 修复 Android 客户端登录页提示「服务不可达」、无法连接服务器的问题(正式版安装包此前缺少联网权限)
## [1.0.19] - 2026-06-07
### 改进
- 优化 Android 构建与部署流程(CI/CD 内部调整),功能与界面无变化
## [1.0.18] - 2026-06-06
### 新功能
- 移动端(Android)界面适配:窄屏(手机)自动切换为移动布局——侧边栏改为侧滑抽屉导航,各列表(库存/出入库/财务/往来单位/基础数据)由宽表格自动切换为卡片流,弹窗与表单自适应屏宽不再溢出;宽屏(桌面/Web)布局保持不变
- Android 客户端正式上线:CI/CD 新增 Android APK 构建(正式 release 签名、versionCode 单调递增),随发版自动发布到下载页;下载页 Android 卡片已激活
### 改进
- 表单字段在手机上占满整行,便于点选;底部状态栏在手机端隐藏
## [1.0.17] - 2026-06-05
### 新功能
- 意见反馈(Bug 反馈 / 功能建议)改为应用内直接提交,支持文字 + 图片(最多 9 张),不再跳转邮件
### 改进
- 「关于」更名为「关于我们」并移至左侧菜单,成为独立页面
- 打印时默认文件名带类型与时间戳(如 标签_20260605_103000、入库单_…、出库单_…),方便「打印到 PDF」保存归档
## [1.0.16] - 2026-06-05
### 新功能
- 应用内更新:Windows/macOS 点「立即更新」直接在应用内下载并自动安装/重启(带进度条),规避浏览器 SmartScreen 下载拦截;Web 为刷新页面
- Windows 安装程序改为中文向导
### 改进
- 「检查更新」按钮增加反馈提示(正在检查 / 已是最新 / 发现新版本 / 检查失败)
- 更新提示文案优化:横幅显示「建议立即更新以获得最新功能与修复」,不再展示冗长更新内容
## [1.0.15] - 2026-06-05
### 修复
- 修复 Windows 桌面端打印卡死:打印字体由 OpenType/CFF 改为 TrueType(glyf)dart_pdf 渲染中文不再抛错(此前点「打印」后卡死)
### 改进
- 「关于我们」内容改为真实信息(服务商:岩美技术有限公司;网站:jiu.yanmei.com;邮箱:yammy2023@163.com),且统一从配置文件读取,不再硬编码
- Windows 桌面端窗口标题由 jiu_client 改为「岩美酒库」
## [1.0.13] - 2026-06-04
### 新功能
- Windows 客户端改为提供安装程序(Inno Setup 打包的 `jiu-windows-x64-setup.exe`,含安装向导、桌面/开始菜单快捷方式、卸载项),不再是解压版 zip
### 修复
- 修复客户端「无法安全下载」:下载地址从内网 HTTP Forgejo`http://192.168.3.200:3000`,公网不可达且被浏览器拦截)改为同源 HTTPS `https://jiu.51yanmei.com/downloads/...`Windows、macOS 均生效
- 部署时将最新安装包发布到 web 服务(`/opt/jiu/downloads/`nginx `/downloads/` 提供),仅保留最新版本(清旧放新)
## [1.0.9] - 2026-06-04
### 修复
- 适配 nginx 容器化部署:nginx 已迁入 `pangolin-edge` 容器(host 网络),deploy 改为将 jiu.conf 写入 pangolin 挂载目录并 `docker exec pangolin-edge nginx -s reload`,不再操作已废弃的主机 nginx
## [1.0.8] - 2026-06-03
### 修复
- 修复 EC2 部署最后一步 nginx reload 失败(`invalid PID number`):改用 `systemctl reload-or-restart nginx`(经 systemd MAINPID 发 HUP,不依赖空的 /run/nginx.pid),并 `enable nginx` 确保重启后自启
## [1.0.7] - 2026-06-03
### 修复
- 修复 Windows 构建 `PathExistsException`errno 183):复制 client/ 到 C:\jiu-build 后,先清除 cp 复制成真实目录的插件软链(windows/flutter/ephemeral)及 .dart_tool/build,再交给 flutter 干净重建
## [1.0.6] - 2026-06-03
### 改进
- CI 运行环境自愈预置:mac/windows runner 首次构建自动 `flutter precache` 并预热 pub/go/npm 缓存(stamp 文件按 Flutter 版本短路),后续构建不再运行时下载 engine
- 发版流水线同一 mac runner 上的任务改为串行(build-macos 依赖 build-linux-web),避免排队等待超时失败
- 暂停定时数据库备份(保留手动触发)
## [1.0.5] - 2026-06-03
### 修复
- 修复 Windows CI 构建失败:放弃脆弱的 `subst W:` 路径映射,改为将 client/ 复制到 System32 之外的干净短路径(C:\jiu-build)构建,规避 WOW64 文件系统重定向
## [1.0.4] - 2026-05-30
### 新功能
- 支持 macOS 桌面客户端下载,下载页 macOS 卡片已激活,下载 zip 解压即用
## [1.0.3] - 2026-05-28
### 新功能
- 新增用户自助注册功能,在官网 /register/ 填写门店信息即可完成注册,系统自动生成门店编码(S000001 格式)
- 注册账号默认设为超级管理员权限
- 支持 Windows 桌面客户端构建与发布
### 修复
- 修复新门店首次登录时库存、仓库、用户等列表加载崩溃的问题
- 修复库存管理页在无数据时页面崩溃(RangeError)的问题
- 商品标签打印改为横向 38mm×20mm 布局,修复标签内容错位问题
- 修复数据导入中移除不必要的"商品编码"导入项
## [1.0.2] - 2026-05-25
### 修复
- 打印入库单/出库单/商品标签时,若浏览器拦截了弹窗,现在会给出明确提示,不再静默失败
## [1.0.1] - 2026-05-25
### 改进
- 库存搜索支持拼音和拼音首字母(例:搜"mt"或"maotai"可匹配"茅台"
- 库存搜索改为回车/点击触发,减少无效请求
- 新增异常自动上报机制,线上错误可实时追踪
## [1.0.0] - 2026-05-24
初始版本发布,完成核心库存管理功能。
+127 -1
View File
@@ -94,7 +94,7 @@ doc-writer
| linter | 格式自动修复(gofmt/dart format),docs/review/lint-report.md | 逻辑代码 |
| code-reviewer | docs/review/*-review.md | 任何代码 |
| security-auditor | docs/security/ | 任何代码 |
| devops | deploy/, .github/workflows/ | 业务代码 |
| devops | deploy/, .gitea/workflows/, scripts/ci/ | 业务代码 |
| sre | docs/runbooks/ | 任何代码 |
| doc-writer | docs/(除 context/ 外) | 任何代码 |
@@ -150,6 +150,45 @@ security: 修复入库单多租户隔离漏洞
---
## 开发完成标准(Definition of Done
**任何功能或修复,必须满足以下全部条件才算完成:**
### 后端变更
```bash
# 1. 编译通过
export PATH="/opt/homebrew/bin:$PATH"
cd backend && go build ./...
# 2. 所有测试通过(不得有 FAIL)
cd backend && go test ./...
# 3. 无编译警告(vet 检查)
cd backend && go vet ./...
```
### 前端变更
```bash
# 1. 静态分析无 errorwarning/info 允许存在)
cd client && flutter analyze --no-fatal-infos --no-fatal-warnings
# 2. 所有测试通过
cd client && flutter test
```
### 通用要求
- [ ] 代码已按 Git 提交规范提交(feat/fix/test/...
- [ ] 涉及新接口:已在对应测试文件中覆盖核心路径
- [ ] 涉及数据库变更:schema.sql 和 model 同步更新
- [ ] 多租户隔离未被破坏(所有查询含 shop_id 条件)
**测试未通过前,禁止提交代码、禁止打 tag 发版。**
---
## 代码质量门禁
以下情况**不得提交**
@@ -201,6 +240,13 @@ security: 修复入库单多租户隔离漏洞
- 不要在 toolbar 放独立的筛选按钮,筛选入口应内嵌在列头
- 列定义使用 `ColDef`,可隐藏列用 `ColumnToggleButton` 控制
### 响应式 / 移动端适配
- 断点判定统一用 `context.isMobile``client/lib/core/responsive/responsive.dart`,宽度 < 600 为窄屏/手机),**禁止**在各处散落 `MediaQuery.width < 600` 这类魔法数
- **列表屏**:用 `DataTableCard` 时必须同时传 `mobileCards`(窄屏渲染卡片流),卡片复用 `MobileListCard` / `MobileCardField``widgets/mobile_list_card.dart`);宽屏仍走原表格,行为不变
- **弹窗固定宽度**:一律用 `context.dialogWidth(X)`(≤ 屏宽 92%),**禁止**裸写 `width: <固定值>` 导致窄屏溢出
- **导航**:窄屏(手机)用 Drawer 抽屉(`app_shell.dart`),宽屏保持侧边栏;新增页面无需单独处理,挂在 shell 下即可
- 平台判断仍遵守上面「前端平台判断」规则(`kIsWeb` 先于 `dart:io`
### 权限中间件(已全局挂载)
- `middleware.ReadOnly()`:已挂载到主 `api` 路由组,只读用户(role=readonly)所有写操作自动返回 403
- `middleware.AdminOnly()`:挂载在 `/users` 路由组,仅管理员可管理用户
@@ -212,3 +258,83 @@ security: 修复入库单多租户隔离漏洞
tx.Set("gorm:query_option", "FOR UPDATE").Where(...).First(&model)
```
- 适用场景:单号生成(`number_rules`)、库存扣减(`inventories`)、任何 check-then-act 模式
### 发版流程
使用 `/release <version>` slash command
```
/release 1.0.2
```
执行顺序:本地 build → test → 更新 CHANGELOG.md → git commit → tag → push main+tag。
CI/CDForgejo`.gitea/workflows/deploy.yml`)收到 tag 后自动:编译 → 测试 → 创建 Release → 部署 EC2 → Telegram 通知。
**多平台构建矩阵**(脚本在 `scripts/ci/`):
- mac runner(容量 1)串行链:`build-linux-web`(后端+Web+官网)→ `build-macos` → `build-android` → `build-ios` → `release-deploy`
- windows runner 并行:`build-windows`Inno Setup 打包 setup.exe
- 分发:桌面(Win/macOS) 发布到下载页 `/downloads/` + 应用内更新;**Android** APK 同样挂 `/downloads/`,需 `ANDROID_*` 签名 secrets(见 `docs/android-signing.md`);**iOS** 走 TestFlight,需 `IOS_*` / `APPSTORE_*` secrets(见 `docs/ios-signing.md`
- Android/iOS 的 job 在对应 secrets 未配置时**优雅跳过**(exit 0,不阻塞发版);本地无 `client/android/key.properties` 时 APK 自动回退 debug 签名
**CHANGELOG 格式**Keep a Changelog):
```markdown
## [1.0.2] - YYYY-MM-DD
### 新功能
- ...
### 改进
- ...
### 修复
- ...
```
只保留有内容的分类。`/release` 会检查 CHANGELOG 是否已有该版本节,没有则从 git log 自动生成。
### 异常上报
客户端异常已在两处统一捕获,**无需**在每个业务层重复处理:
- `main.dart``FlutterError.onError` + `runZonedGuarded` 捕获所有未处理异常
- `api_client.dart`:Dio 拦截器自动上报所有 HTTP 5xx 错误
需要**手动调用** `reportError(e, st)` 的场景:
- 技术性异常(JSON 解析失败、第三方 SDK 崩溃等)
- **不需要**上报:`AppException` 及其子类(已知业务错误)
```dart
} catch (e, st) {
reportError(e, st); // 一行,fire-and-forget
rethrow;
}
```
### 项目 TODO 管理
**唯一待办系统**:任务执行过程中如发现需要做的事项,**必须**用 `/todo add <描述>` 记入项目 todo 系统,**禁止**使用 TaskCreate/TodoWrite 等其它 todo 工具,保持单一真相源。
- **真相源**`todo/todo.json`(结构化 JSON,含 id/标题/重要度/平台标签/创建时间/完成版本)
- **HTML**`todo/todo.html` 为生成物,每次写操作自动重渲染
- **重要度**`high`(阻断/紧急)/ `mid`(重要)/ `low`(一般/优化)三级
- **完成即标记**:完成对应 todo 后运行 `/todo done <id>`,脚本自动记入当前 git tag 版本
常用命令:
```
/todo — 查看 summary(含未完成分级列表)
/todo add 修复... — 新增待办(Claude 自动推断级别/标签)
/todo done <id> — 标记完成,自动记入版本号
/todo reopen <id> — 撤销完成
/todo rm <id> — 删除
```
底层脚本:`node todo/todo.mjs <子命令> [参数]`
---
### 搜索实现(拼音)
商品名搜索同时支持汉字、全拼、首字母:
- `products` 表有 `name_pinyin`(全拼)和 `name_initials`(首字母)两列
- Create / Update / FindOrCreate 写入时自动调用 `util.ToPinyin()` 生成,**无需手动维护**
- 启动时对 `name_pinyin = ''` 的存量数据自动回填(`backfillPinyin`
- 库存搜索 SQL 同时 LIKE 匹配 name、code、name_pinyin、name_initials
- 新增其他需要拼音搜索的实体时,复用 `backend/internal/util/pinyin.go` 的 `ToPinyin()`
+4 -4
View File
@@ -1,10 +1,10 @@
version: "1.0.0"
build_number: 1
version: "1.0.3"
build_number: 4
force_update: false
release_notes: "初始版本发布,完成核心库存管理功能。"
release_notes: "新增用户自助注册功能;商品标签打印修复竖向 20mm×38mm 布局"
download_urls:
macos: ""
windows: ""
ios: ""
android: ""
android: "https://jiu.51yanmei.com/downloads/jiu-android.apk"
web: "https://jiu.51yanmei.com/app"
+1
View File
@@ -44,6 +44,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/go-pinyin v0.21.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
+2
View File
@@ -73,6 +73,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mozillazg/go-pinyin v0.21.0 h1:Wo8/NT45z7P3er/9YSLHA3/kjZzbLz5hR7i+jGeIGao=
github.com/mozillazg/go-pinyin v0.21.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+17
View File
@@ -49,6 +49,23 @@ func (h *AuthHandler) Login(c *gin.Context) {
})
}
// Register POST /api/v1/public/register
func (h *AuthHandler) Register(c *gin.Context) {
var req service.RegisterInput
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := h.svc.Register(req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
// Refresh POST /api/v1/auth/refresh
func (h *AuthHandler) Refresh(c *gin.Context) {
var req struct {
+118
View File
@@ -0,0 +1,118 @@
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/internal/model"
)
type ErrorReportHandler struct{ db *gorm.DB }
func NewErrorReportHandler(db *gorm.DB) *ErrorReportHandler {
return &ErrorReportHandler{db: db}
}
type submitErrorRequest struct {
ErrorType string `json:"error_type" binding:"required"`
AppVersion string `json:"app_version"`
Platform string `json:"platform"`
Username string `json:"username"`
ShopID uint64 `json:"shop_id"`
ShopNo string `json:"shop_no"`
Role string `json:"role"`
ProductInfo string `json:"product_info"`
ErrorMsg string `json:"error_msg" binding:"required"`
StackTrace string `json:"stack_trace"`
OccurredAt int64 `json:"occurred_at"` // Unix 毫秒
}
// Submit POST /api/v1/public/errors
func (h *ErrorReportHandler) Submit(c *gin.Context) {
var req submitErrorRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
occurredAt := time.Now()
if req.OccurredAt > 0 {
occurredAt = time.UnixMilli(req.OccurredAt)
}
stack := req.StackTrace
const maxStack = 4000
if len(stack) > maxStack {
stack = stack[:maxStack]
}
report := model.ErrorReport{
ErrorType: req.ErrorType,
AppVersion: req.AppVersion,
Platform: req.Platform,
Username: req.Username,
ShopID: req.ShopID,
ShopNo: req.ShopNo,
Role: req.Role,
ProductInfo: req.ProductInfo,
ClientIP: c.ClientIP(),
ErrorMsg: req.ErrorMsg,
StackTrace: stack,
OccurredAt: occurredAt,
}
if err := h.db.Create(&report).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "submit failed"})
return
}
c.JSON(http.StatusCreated, gin.H{"id": report.ID})
}
// List GET /api/v1/admin/errors (superadmin only)
func (h *ErrorReportHandler) List(c *gin.Context) {
var q struct {
ErrorType string `form:"error_type"`
Username string `form:"username"`
Platform string `form:"platform"`
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=50"`
}
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if q.PageSize <= 0 || q.PageSize > 200 {
q.PageSize = 50
}
if q.Page <= 0 {
q.Page = 1
}
db := h.db.Model(&model.ErrorReport{})
if q.ErrorType != "" {
db = db.Where("error_type = ?", q.ErrorType)
}
if q.Username != "" {
db = db.Where("username LIKE ?", "%"+q.Username+"%")
}
if q.Platform != "" {
db = db.Where("platform = ?", q.Platform)
}
var total int64
db.Count(&total)
var reports []model.ErrorReport
db.Order("id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Find(&reports)
c.JSON(http.StatusOK, gin.H{
"data": reports,
"total": total,
"page": q.Page,
"page_size": q.PageSize,
})
}
+198
View File
@@ -0,0 +1,198 @@
package handler
import (
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
)
type FeedbackHandler struct{ db *gorm.DB }
func NewFeedbackHandler(db *gorm.DB) *FeedbackHandler {
return &FeedbackHandler{db: db}
}
type submitFeedbackRequest struct {
Type string `json:"type" binding:"required,oneof=bug suggestion"`
Content string `json:"content"`
Images []string `json:"images"`
AppVersion string `json:"app_version"`
Platform string `json:"platform"`
}
// Submit POST /api/v1/feedback (需登录)
func (h *FeedbackHandler) Submit(c *gin.Context) {
shopID := middleware.GetShopID(c)
userID := middleware.GetUserID(c)
role := c.GetString(middleware.CtxRole)
var req submitFeedbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if strings.TrimSpace(req.Content) == "" && len(req.Images) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写反馈内容或添加图片"})
return
}
if len(req.Images) > 9 {
req.Images = req.Images[:9]
}
// 去规范化:从 JWT 身份补全 username / shop_code,便于后台查看
var username string
var u model.User
if err := h.db.Select("username").First(&u, userID).Error; err == nil {
username = u.Username
}
var shopCode string
var shop model.Shop
if err := h.db.Select("code").First(&shop, shopID).Error; err == nil {
shopCode = shop.Code
}
fb := model.Feedback{
ShopID: shopID,
ShopCode: shopCode,
UserID: userID,
Username: username,
Role: role,
Type: req.Type,
Content: strings.TrimSpace(req.Content),
Images: req.Images,
AppVersion: req.AppVersion,
Platform: req.Platform,
ClientIP: c.ClientIP(),
Status: "new",
}
if err := h.db.Create(&fb).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "submit failed"})
return
}
c.JSON(http.StatusCreated, gin.H{"id": fb.ID})
}
// UploadImage POST /api/v1/feedback/images (需登录)
func (h *FeedbackHandler) UploadImage(c *gin.Context) {
shopID := middleware.GetShopID(c)
// 限制 5MB
if err := c.Request.ParseMultipartForm(5 << 20); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件超过 5MB 限制"})
return
}
file, _, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请上传文件(field: file"})
return
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "仅支持 JPEG/PNG 图片"})
return
}
resized := imaging.Fit(img, 1600, 1600, imaging.Lanczos)
filename := uuid.New().String() + ".jpg"
subdir := fmt.Sprintf("%s/feedback/%d", config.C.Storage.UploadDir, shopID)
if err := os.MkdirAll(subdir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "存储目录创建失败"})
return
}
fullPath := filepath.Join(subdir, filename)
if err := imaging.Save(resized, fullPath, imaging.JPEGQuality(85)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "图片保存失败"})
return
}
relURL := fmt.Sprintf("/images/feedback/%d/%s", shopID, filename)
c.JSON(http.StatusCreated, gin.H{"url": relURL})
}
// List GET /api/v1/admin/feedback (仅超级管理员)
func (h *FeedbackHandler) List(c *gin.Context) {
var q struct {
Type string `form:"type"`
Status string `form:"status"`
ShopID uint64 `form:"shop_id"`
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=50"`
}
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if q.PageSize <= 0 || q.PageSize > 200 {
q.PageSize = 50
}
if q.Page <= 0 {
q.Page = 1
}
db := h.db.Model(&model.Feedback{})
if q.Type != "" {
db = db.Where("type = ?", q.Type)
}
if q.Status != "" {
db = db.Where("status = ?", q.Status)
}
if q.ShopID != 0 {
db = db.Where("shop_id = ?", q.ShopID)
}
var total int64
db.Count(&total)
var list []model.Feedback
db.Order("id DESC").Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).Find(&list)
c.JSON(http.StatusOK, gin.H{
"data": list,
"total": total,
"page": q.Page,
"page_size": q.PageSize,
})
}
// UpdateStatus PATCH /api/v1/admin/feedback/:id (仅超级管理员)
func (h *FeedbackHandler) UpdateStatus(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var req struct {
Status string `json:"status" binding:"required,oneof=new handled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
res := h.db.Model(&model.Feedback{}).Where("id = ?", id).Update("status", req.Status)
if res.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": res.Error.Error()})
return
}
if res.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "updated"})
}
+72
View File
@@ -0,0 +1,72 @@
package handler
import (
"net/http"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/testutil"
)
func TestFeedbackHandler_SubmitAndList(t *testing.T) {
db := testutil.SetupTestDB()
require.NoError(t, db.AutoMigrate(&model.Feedback{}))
shop := testutil.CreateTestShop(db, "FB001")
user := testutil.CreateTestUser(db, shop.ID, "fbuser", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
fh := NewFeedbackHandler(db)
r := gin.New()
r.Use(gin.Recovery())
api := r.Group("/api/v1")
api.Use(middleware.JWT())
api.POST("/feedback", fh.Submit)
adminG := api.Group("/admin")
adminG.Use(middleware.SuperAdminOnly())
adminG.GET("/feedback", fh.List)
// 1. 正常提交(文字 + 图片)
w := makeRequest(r, "POST", "/api/v1/feedback", token, map[string]interface{}{
"type": "bug",
"content": "打印时卡住了",
"images": []string{"/images/feedback/1/a.jpg"},
})
require.Equal(t, http.StatusCreated, w.Code)
assert.NotZero(t, parseResponse(w)["id"])
// 去规范化字段已写入
var fb model.Feedback
require.NoError(t, db.Order("id DESC").First(&fb).Error)
assert.Equal(t, "fbuser", fb.Username)
assert.Equal(t, "FB001", fb.ShopCode)
assert.Equal(t, "new", fb.Status)
assert.Equal(t, 1, len(fb.Images))
// 2. 内容与图片都为空 → 400
w = makeRequest(r, "POST", "/api/v1/feedback", token, map[string]interface{}{
"type": "suggestion",
})
assert.Equal(t, http.StatusBadRequest, w.Code)
// 3. 非法 type → 400
w = makeRequest(r, "POST", "/api/v1/feedback", token, map[string]interface{}{
"type": "other",
"content": "x",
})
assert.Equal(t, http.StatusBadRequest, w.Code)
// 4. 普通管理员访问 admin 列表 → 403
w = makeRequest(r, "GET", "/api/v1/admin/feedback", token, nil)
assert.Equal(t, http.StatusForbidden, w.Code)
// 5. 超级管理员可列出
superToken := getAuthToken(user.ID, shop.ID, "superadmin")
w = makeRequest(r, "GET", "/api/v1/admin/feedback", superToken, nil)
require.Equal(t, http.StatusOK, w.Code)
assert.GreaterOrEqual(t, parseResponse(w)["total"].(float64), float64(1))
}
+1 -1
View File
@@ -51,7 +51,7 @@ func (h *FinanceHandler) ListRecords(c *gin.Context) {
var total int64
base.Count(&total)
var records []model.FinanceRecord
records := make([]model.FinanceRecord, 0)
offset := (q.Page - 1) * q.PageSize
base.Preload("Partner").Order("record_date DESC, id DESC").Offset(offset).Limit(q.PageSize).Find(&records)
+3 -3
View File
@@ -64,9 +64,9 @@ func (h *InventoryHandler) List(c *gin.Context) {
inStock := c.Query("in_stock")
if keyword != "" {
baseWhere += " AND (COALESCE(NULLIF(p.name,''), inv.product_name) LIKE ? OR COALESCE(NULLIF(p.code,''), inv.product_code) LIKE ?)"
baseWhere += " AND (COALESCE(NULLIF(p.name,''), inv.product_name) LIKE ? OR COALESCE(NULLIF(p.code,''), inv.product_code) LIKE ? OR p.name_pinyin LIKE ? OR p.name_initials LIKE ?)"
like := "%" + keyword + "%"
args = append(args, like, like)
args = append(args, like, like, like, like)
}
if warehouseIDStr != "" {
baseWhere += " AND inv.warehouse_id = ?"
@@ -138,7 +138,7 @@ func (h *InventoryHandler) Logs(c *gin.Context) {
var total int64
query.Count(&total)
var logs []model.InventoryLog
logs := make([]model.InventoryLog, 0)
query.Preload("Product").Preload("Warehouse").Offset((page-1)*pageSize).Limit(pageSize).Order("id DESC").Find(&logs)
c.JSON(http.StatusOK, gin.H{"data": logs, "total": total, "page": page, "page_size": pageSize})
+1 -1
View File
@@ -32,7 +32,7 @@ var defaultRules = []struct {
// List GET /api/v1/number-rules
func (h *NumberRuleHandler) List(c *gin.Context) {
shopID := middleware.GetShopID(c)
var rules []model.NumberRule
rules := make([]model.NumberRule, 0)
h.db.Where("shop_id = ?", shopID).Order("id").Find(&rules)
if len(rules) == 0 {
+1 -1
View File
@@ -38,7 +38,7 @@ func (h *PartnerHandler) List(c *gin.Context) {
var total int64
query.Count(&total)
var partners []model.Partner
partners := make([]model.Partner, 0)
query.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&partners)
c.JSON(http.StatusOK, gin.H{"data": partners, "total": total, "page": page, "page_size": pageSize})
+62 -24
View File
@@ -15,6 +15,7 @@ import (
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/internal/util"
)
type ProductHandler struct {
@@ -47,7 +48,7 @@ func (h *ProductHandler) List(c *gin.Context) {
var total int64
query.Count(&total)
var products []model.Product
products := make([]model.Product, 0)
offset := (page - 1) * pageSize
query.Preload("Category").Offset(offset).Limit(pageSize).
Order("id DESC").Find(&products)
@@ -70,6 +71,7 @@ func (h *ProductHandler) Create(c *gin.Context) {
}
product.ShopID = shopID
product.PublicID = uuid.New().String()
product.NamePinyin, product.NameInitials = util.ToPinyin(product.Name)
// Auto-generate product code if not provided (e.g. P001, P002)
// Retry up to 5 times on duplicate key to handle concurrent creates
@@ -122,22 +124,29 @@ func (h *ProductHandler) Update(c *gin.Context) {
return
}
namePinyin, nameInitials := util.ToPinyin(req.Name)
// 只更新业务字段,防止 Save() 覆盖 shop_id / created_at 等系统字段
if err := h.db.Model(&product).Updates(map[string]interface{}{
"code": req.Code,
"barcode": req.Barcode,
"name": req.Name,
"series": req.Series,
"spec": req.Spec,
"unit": req.Unit,
"category_id": req.CategoryID,
"brand": req.Brand,
"purchase_price": req.PurchasePrice,
"sale_price": req.SalePrice,
"min_stock": req.MinStock,
"description": req.Description,
"remark": req.Remark,
"custom_fields": req.CustomFields,
"code": req.Code,
"barcode": req.Barcode,
"name": req.Name,
"series": req.Series,
"spec": req.Spec,
"unit": req.Unit,
"category_id": req.CategoryID,
"brand": req.Brand,
"purchase_price": req.PurchasePrice,
"sale_price": req.SalePrice,
"min_stock": req.MinStock,
"description": req.Description,
"remark": req.Remark,
"custom_fields": req.CustomFields,
"name_pinyin": namePinyin,
"name_initials": nameInitials,
"origin_id": req.OriginID,
"shelf_life_id": req.ShelfLifeID,
"storage_id": req.StorageID,
"description_doc_id": req.DescriptionDocID,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -156,6 +165,7 @@ func (h *ProductHandler) Detail(c *gin.Context) {
var product model.Product
if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", id, shopID).
Preload("Category").Preload("Images").
Preload("Origin").Preload("ShelfLife").Preload("Storage").
First(&product).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -203,9 +213,13 @@ func (h *ProductHandler) QRCode(c *gin.Context) {
func (h *ProductHandler) FindOrCreate(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Name string `json:"name" binding:"required"`
Series string `json:"series"`
Spec string `json:"spec"`
Name string `json:"name" binding:"required"`
Series string `json:"series"`
Spec string `json:"spec"`
OriginID *uint64 `json:"origin_id"`
ShelfLifeID *uint64 `json:"shelf_life_id"`
StorageID *uint64 `json:"storage_id"`
DescriptionDocID *uint64 `json:"description_doc_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -216,6 +230,23 @@ func (h *ProductHandler) FindOrCreate(c *gin.Context) {
err := h.db.Where("shop_id = ? AND name = ? AND series = ? AND spec = ? AND deleted_at IS NULL",
shopID, req.Name, req.Series, req.Spec).First(&product).Error
if err == nil {
// 商品已存在:如果传入了属性 ID,则更新属性关联
attrUpdates := map[string]interface{}{}
if req.OriginID != nil {
attrUpdates["origin_id"] = req.OriginID
}
if req.ShelfLifeID != nil {
attrUpdates["shelf_life_id"] = req.ShelfLifeID
}
if req.StorageID != nil {
attrUpdates["storage_id"] = req.StorageID
}
if req.DescriptionDocID != nil {
attrUpdates["description_doc_id"] = req.DescriptionDocID
}
if len(attrUpdates) > 0 {
h.db.Model(&product).Updates(attrUpdates)
}
c.JSON(http.StatusOK, gin.H{"data": product})
return
}
@@ -226,13 +257,20 @@ func (h *ProductHandler) FindOrCreate(c *gin.Context) {
var count int64
h.db.Model(&model.Product{}).Where("shop_id = ? AND deleted_at IS NULL", shopID).Count(&count)
namePinyin, nameInitials := util.ToPinyin(req.Name)
product = model.Product{
TenantBase: model.TenantBase{ShopID: shopID},
PublicID: uuid.New().String(),
Name: req.Name,
Series: req.Series,
Spec: req.Spec,
Code: fmt.Sprintf("P%03d", count+1),
TenantBase: model.TenantBase{ShopID: shopID},
PublicID: uuid.New().String(),
Name: req.Name,
Series: req.Series,
Spec: req.Spec,
Code: fmt.Sprintf("P%03d", count+1),
NamePinyin: namePinyin,
NameInitials: nameInitials,
OriginID: req.OriginID,
ShelfLifeID: req.ShelfLifeID,
StorageID: req.StorageID,
DescriptionDocID: req.DescriptionDocID,
}
if createErr := h.db.Create(&product).Error; createErr != nil {
// Race condition: try to find the record created by another request
+268
View File
@@ -0,0 +1,268 @@
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/model"
)
// ProductAttrHandler 处理商品属性字典(产地/保质期/储存方式/描述文档)
type ProductAttrHandler struct {
db *gorm.DB
}
func NewProductAttrHandler(db *gorm.DB) *ProductAttrHandler {
return &ProductAttrHandler{db: db}
}
// ── 产地(origins) ──────────────────────────────────────────────
func (h *ProductAttrHandler) ListOrigins(c *gin.Context) {
shopID := middleware.GetShopID(c)
items := make([]model.ProductOriginOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProductAttrHandler) CreateOrigin(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := model.ProductOriginOption{
TenantBase: model.TenantBase{ShopID: shopID},
Code: req.Code,
Name: req.Name,
Remark: req.Remark,
}
if err := h.db.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
}
func (h *ProductAttrHandler) UpdateOrigin(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var item model.ProductOriginOption
if err := h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).First(&item).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
item.Code = req.Code
item.Name = req.Name
item.Remark = req.Remark
h.db.Save(&item)
c.JSON(http.StatusOK, gin.H{"data": item})
}
func (h *ProductAttrHandler) DeleteOrigin(c *gin.Context) {
shopID := middleware.GetShopID(c)
h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).Delete(&model.ProductOriginOption{})
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ── 保质期(shelf-lives) ─────────────────────────────────────────
func (h *ProductAttrHandler) ListShelfLives(c *gin.Context) {
shopID := middleware.GetShopID(c)
items := make([]model.ProductShelfLifeOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProductAttrHandler) CreateShelfLife(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := model.ProductShelfLifeOption{
TenantBase: model.TenantBase{ShopID: shopID},
Code: req.Code,
Name: req.Name,
Remark: req.Remark,
}
if err := h.db.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
}
func (h *ProductAttrHandler) UpdateShelfLife(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var item model.ProductShelfLifeOption
if err := h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).First(&item).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
item.Code = req.Code
item.Name = req.Name
item.Remark = req.Remark
h.db.Save(&item)
c.JSON(http.StatusOK, gin.H{"data": item})
}
func (h *ProductAttrHandler) DeleteShelfLife(c *gin.Context) {
shopID := middleware.GetShopID(c)
h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).Delete(&model.ProductShelfLifeOption{})
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ── 储存方式(storages) ──────────────────────────────────────────
func (h *ProductAttrHandler) ListStorages(c *gin.Context) {
shopID := middleware.GetShopID(c)
items := make([]model.ProductStorageOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProductAttrHandler) CreateStorage(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := model.ProductStorageOption{
TenantBase: model.TenantBase{ShopID: shopID},
Code: req.Code,
Name: req.Name,
Remark: req.Remark,
}
if err := h.db.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
}
func (h *ProductAttrHandler) UpdateStorage(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Code string `json:"code"`
Name string `json:"name" binding:"required"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var item model.ProductStorageOption
if err := h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).First(&item).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
item.Code = req.Code
item.Name = req.Name
item.Remark = req.Remark
h.db.Save(&item)
c.JSON(http.StatusOK, gin.H{"data": item})
}
func (h *ProductAttrHandler) DeleteStorage(c *gin.Context) {
shopID := middleware.GetShopID(c)
h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).Delete(&model.ProductStorageOption{})
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ── 描述文档(description-docs) ─────────────────────────────────
func (h *ProductAttrHandler) ListDescriptionDocs(c *gin.Context) {
shopID := middleware.GetShopID(c)
items := make([]model.ProductDescriptionDoc, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
func (h *ProductAttrHandler) CreateDescriptionDoc(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Title string `json:"title" binding:"required"`
Content string `json:"content"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := model.ProductDescriptionDoc{
TenantBase: model.TenantBase{ShopID: shopID},
Title: req.Title,
Content: req.Content,
Remark: req.Remark,
}
if err := h.db.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": item})
}
func (h *ProductAttrHandler) UpdateDescriptionDoc(c *gin.Context) {
shopID := middleware.GetShopID(c)
var req struct {
Title string `json:"title" binding:"required"`
Content string `json:"content"`
Remark string `json:"remark"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var item model.ProductDescriptionDoc
if err := h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).First(&item).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
item.Title = req.Title
item.Content = req.Content
item.Remark = req.Remark
h.db.Save(&item)
c.JSON(http.StatusOK, gin.H{"data": item})
}
func (h *ProductAttrHandler) DeleteDescriptionDoc(c *gin.Context) {
shopID := middleware.GetShopID(c)
h.db.Where("id = ? AND shop_id = ?", c.Param("id"), shopID).Delete(&model.ProductDescriptionDoc{})
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
+3 -3
View File
@@ -22,7 +22,7 @@ func NewProductOptionHandler(db *gorm.DB) *ProductOptionHandler {
func (h *ProductOptionHandler) ListNames(c *gin.Context) {
shopID := middleware.GetShopID(c)
var items []model.ProductNameOption
items := make([]model.ProductNameOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
@@ -84,7 +84,7 @@ func (h *ProductOptionHandler) DeleteName(c *gin.Context) {
func (h *ProductOptionHandler) ListSeries(c *gin.Context) {
shopID := middleware.GetShopID(c)
var items []model.ProductSeriesOption
items := make([]model.ProductSeriesOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
@@ -146,7 +146,7 @@ func (h *ProductOptionHandler) DeleteSeries(c *gin.Context) {
func (h *ProductOptionHandler) ListSpecs(c *gin.Context) {
shopID := middleware.GetShopID(c)
var items []model.ProductSpecOption
items := make([]model.ProductSpecOption, 0)
h.db.Where("shop_id = ?", shopID).Order("id ASC").Find(&items)
c.JSON(http.StatusOK, gin.H{"data": items})
}
+191 -10
View File
@@ -1,7 +1,11 @@
package handler
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -9,6 +13,12 @@ import (
"github.com/wangjia/jiu/backend/internal/model"
)
// 默认话术(保质期/储存方式空时使用)
const (
defaultShelfLife = "无限期(适饮)"
defaultStorage = "阴凉干燥、避光保存"
)
type PublicHandler struct {
db *gorm.DB
}
@@ -24,6 +34,10 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
var product model.Product
if err := h.db.Where("public_id = ? AND deleted_at IS NULL", publicID).
Preload("Images").
Preload("Origin").
Preload("ShelfLife").
Preload("Storage").
Preload("DescriptionDoc").
First(&product).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
@@ -39,6 +53,7 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
"address": shop.Address,
"phone": shop.Phone,
"business_hours": shop.BusinessHours,
"wechat_id": shop.WechatID,
}
}
@@ -57,25 +72,105 @@ func (h *PublicHandler) GetProduct(c *gin.Context) {
"production_date": pdStr,
"batch_no": inv.BatchNo,
"in_stock_date": inv.CreatedAt.Format("2006-01-02"),
"quantity": inv.Quantity,
}
}
// 产地:空串表示无,前端不展示该行
origin := ""
if product.Origin != nil {
origin = product.Origin.Name
}
// 保质期:无关联时用默认话术
shelfLife := defaultShelfLife
if product.ShelfLife != nil {
shelfLife = product.ShelfLife.Name
}
// 储存方式:无关联时用默认话术
storage := defaultStorage
if product.Storage != nil {
storage = product.Storage.Name
}
// 介绍三级回退:描述文档 → 旧 Description → 通用中性兜底
descTitle, descBody, descKeywords := buildDescription(product)
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"id": product.ID,
"name": product.Name,
"series": product.Series,
"spec": product.Spec,
"brand": product.Brand,
"unit": product.Unit,
"description": product.Description,
"images": product.Images,
"shop": shopData,
"batch": batchData,
"id": product.ID,
"name": product.Name,
"code": product.Code,
"barcode": product.Barcode,
"series": product.Series,
"spec": product.Spec,
"brand": product.Brand,
"unit": product.Unit,
"sale_price": product.SalePrice,
"description": descBody,
"description_title": descTitle,
"description_keywords": descKeywords,
"origin": origin,
"shelf_life": shelfLife,
"storage": storage,
"images": product.Images,
"shop": shopData,
"batch": batchData,
},
})
}
// buildDescription 商品介绍三级回退逻辑
// 1. DescriptionDoc.Content(结构化介绍文档)
// 2. Product.Description(旧自由文本)
// 3. 通用中性兜底(用真实字段拼,不含品牌专属词)
func buildDescription(p model.Product) (title, body string, keywords []string) {
// 1. 描述文档
if p.DescriptionDoc != nil && p.DescriptionDoc.Content != "" {
return p.DescriptionDoc.Title, p.DescriptionDoc.Content, buildKeywords(p)
}
// 2. 旧 Description 自由文本
if p.Description != "" {
return "商品介绍", p.Description, buildKeywords(p)
}
// 3. 通用中性兜底
parts := make([]string, 0, 4)
if p.Brand != "" && p.Name != "" {
parts = append(parts, fmt.Sprintf("%s%s", p.Brand, p.Name))
} else if p.Name != "" {
parts = append(parts, p.Name)
}
if p.Series != "" {
parts = append(parts, fmt.Sprintf("%s系列", p.Series))
}
if p.Spec != "" {
parts = append(parts, fmt.Sprintf("规格%s", p.Spec))
}
intro := strings.Join(parts, "")
if intro != "" {
intro += "。"
}
intro += "本商品由门店正品供应,支持扫码验真,请认准官方渠道。"
return "商品介绍", intro, buildKeywords(p)
}
// buildKeywords 从真实商品属性派生关键词 chips
func buildKeywords(p model.Product) []string {
kws := make([]string, 0, 4)
if p.Brand != "" {
kws = append(kws, p.Brand)
}
if p.Series != "" {
kws = append(kws, p.Series)
}
kws = append(kws, "正品保障", "扫码验真")
return kws
}
// GetRelease GET /api/v1/public/release (no auth)
func (h *PublicHandler) GetRelease(c *gin.Context) {
cfg, err := loadVersionConfig()
@@ -93,3 +188,89 @@ func (h *PublicHandler) GetRelease(c *gin.Context) {
"download_urls": cfg.DownloadURLs,
})
}
type publicProductImage struct {
URL string `json:"url"`
}
type publicProductResp struct {
ID uint64 `json:"id"`
PublicID string `json:"public_id"`
Name string `json:"name"`
Series string `json:"series"`
Spec string `json:"spec"`
Brand string `json:"brand"`
Unit string `json:"unit"`
SalePrice float64 `json:"sale_price"`
Images []publicProductImage `json:"images"`
}
// ListShopProducts GET /api/v1/public/shops/:shop_code/products (no auth)
func (h *PublicHandler) ListShopProducts(c *gin.Context) {
shopCode := c.Param("shop_code")
var shop model.Shop
if err := h.db.Where("code = ? AND deleted_at IS NULL", shopCode).First(&shop).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "shop not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 50 {
pageSize = 20
}
query := h.db.Model(&model.Product{}).
Where("shop_id = ? AND public_id IS NOT NULL AND public_id != '' AND deleted_at IS NULL", shop.ID)
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var products []model.Product
offset := (page - 1) * pageSize
if err := query.Preload("Images").
Offset(offset).
Limit(pageSize).
Order("id DESC").
Find(&products).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
listData := make([]publicProductResp, len(products))
for i, p := range products {
imgs := make([]publicProductImage, len(p.Images))
for j, img := range p.Images {
imgs[j] = publicProductImage{URL: img.URL}
}
listData[i] = publicProductResp{
ID: p.ID,
PublicID: p.PublicID,
Name: p.Name,
Series: p.Series,
Spec: p.Spec,
Brand: p.Brand,
Unit: p.Unit,
SalePrice: p.SalePrice,
Images: imgs,
}
}
c.JSON(http.StatusOK, gin.H{
"data": listData,
"total": total,
"page": page,
"page_size": pageSize,
})
}
+2
View File
@@ -47,6 +47,7 @@ func (h *ShopHandler) UpdateInfo(c *gin.Context) {
Phone string `json:"phone"`
ManagerName string `json:"manager_name"`
LogoURL string `json:"logo_url"`
WechatID string `json:"wechat_id"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@@ -58,6 +59,7 @@ func (h *ShopHandler) UpdateInfo(c *gin.Context) {
"address": req.Address,
"phone": req.Phone,
"manager_name": req.ManagerName,
"wechat_id": req.WechatID,
}
if req.LogoURL != "" {
updates["logo_url"] = req.LogoURL
+4 -2
View File
@@ -50,7 +50,7 @@ func (h *StockInHandler) List(c *gin.Context) {
var total int64
query.Count(&total)
var orders []model.StockInOrder
orders := make([]model.StockInOrder, 0)
query.Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
Offset((page - 1) * pageSize).Limit(pageSize).
Order("id DESC").Find(&orders)
@@ -62,7 +62,9 @@ func (h *StockInHandler) List(c *gin.Context) {
func (h *StockInHandler) Get(c *gin.Context) {
shopID := middleware.GetShopID(c)
var order model.StockInOrder
if err := h.db.Preload("Items.Product").Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
if err := h.db.Preload("Items.Product").Preload("Items.Product.Origin").
Preload("Items.Product.ShelfLife").Preload("Items.Product.Storage").
Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID).
First(&order).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
+1 -1
View File
@@ -44,7 +44,7 @@ func (h *StockOutHandler) List(c *gin.Context) {
var total int64
query.Count(&total)
var orders []model.StockOutOrder
orders := make([]model.StockOutOrder, 0)
query.Preload("Warehouse").Preload("Partner").Preload("Operator").Preload("Reviewer").
Offset((page - 1) * pageSize).Limit(pageSize).
Order("id DESC").Find(&orders)
+1 -1
View File
@@ -22,7 +22,7 @@ func NewUserHandler(db *gorm.DB) *UserHandler {
// List GET /api/v1/users
func (h *UserHandler) List(c *gin.Context) {
shopID := middleware.GetShopID(c)
var users []model.User
users := make([]model.User, 0)
h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID).
Order("id ASC").Find(&users)
c.JSON(http.StatusOK, gin.H{"data": users})
+1 -1
View File
@@ -20,7 +20,7 @@ func NewWarehouseHandler(db *gorm.DB) *WarehouseHandler {
func (h *WarehouseHandler) List(c *gin.Context) {
shopID := middleware.GetShopID(c)
var warehouses []model.Warehouse
warehouses := make([]model.Warehouse, 0)
h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID).Find(&warehouses)
c.JSON(http.StatusOK, gin.H{"data": warehouses})
}
+1 -1
View File
@@ -51,7 +51,7 @@ func JWT() gin.HandlerFunc {
func AdminOnly() gin.HandlerFunc {
return func(c *gin.Context) {
role, _ := c.Get(CtxRole)
if role != "admin" {
if role != "admin" && role != "superadmin" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "admin only"})
return
}
+21
View File
@@ -0,0 +1,21 @@
package model
import "time"
// ErrorReport 客户端错误上报记录(系统级,不做多租户隔离)
type ErrorReport struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
CreatedAt time.Time `json:"created_at"`
ErrorType string `gorm:"size:30;not null;index" json:"error_type"` // flutter_error / zone_error / caught_exception
AppVersion string `gorm:"size:30" json:"app_version"`
Platform string `gorm:"size:20" json:"platform"` // ios/android/web/windows/macos
Username string `gorm:"size:50;index" json:"username"`
ShopID uint64 `gorm:"index" json:"shop_id"`
ShopNo string `gorm:"size:50" json:"shop_no"`
Role string `gorm:"size:20" json:"role"`
ProductInfo string `gorm:"size:200" json:"product_info"`
ClientIP string `gorm:"size:60" json:"client_ip"`
ErrorMsg string `gorm:"type:text;not null" json:"error_msg"`
StackTrace string `gorm:"type:text" json:"stack_trace"`
OccurredAt time.Time `json:"occurred_at"`
}
+58
View File
@@ -0,0 +1,58 @@
package model
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
)
// StringSlice 以 JSON 数组形式存储的字符串切片(用于图片 URL 列表)
type StringSlice []string
func (s StringSlice) Value() (driver.Value, error) {
if s == nil {
return "[]", nil
}
b, err := json.Marshal(s)
return string(b), err
}
func (s *StringSlice) Scan(value interface{}) error {
if value == nil {
*s = nil
return nil
}
var b []byte
switch v := value.(type) {
case string:
b = []byte(v)
case []byte:
b = v
default:
return fmt.Errorf("cannot scan %T into StringSlice", value)
}
if len(b) == 0 {
*s = nil
return nil
}
return json.Unmarshal(b, s)
}
// Feedback 用户意见反馈(bug / 功能建议):文字 + 附图。多租户按 ShopID 隔离。
type Feedback struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
CreatedAt time.Time `json:"created_at"`
ShopID uint64 `gorm:"index;not null" json:"shop_id"`
ShopCode string `gorm:"size:50" json:"shop_code"`
UserID uint64 `gorm:"index" json:"user_id"`
Username string `gorm:"size:50" json:"username"`
Role string `gorm:"size:20" json:"role"`
Type string `gorm:"size:20;not null;index" json:"type"` // bug / suggestion
Content string `gorm:"type:text" json:"content"`
Images StringSlice `gorm:"type:json" json:"images"`
AppVersion string `gorm:"size:30" json:"app_version"`
Platform string `gorm:"size:20" json:"platform"`
ClientIP string `gorm:"size:60" json:"client_ip"`
Status string `gorm:"size:20;not null;default:new;index" json:"status"` // new / handled
}
+16 -4
View File
@@ -22,9 +22,21 @@ type Product struct {
SalePrice float64 `gorm:"type:decimal(12,2)" json:"sale_price"`
MinStock int `gorm:"default:0" json:"min_stock"`
Description string `gorm:"type:text" json:"description"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
Remark string `gorm:"size:500" json:"remark"`
NamePinyin string `gorm:"size:400;index" json:"-"`
NameInitials string `gorm:"size:100;index" json:"-"`
Category *ProductCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Images []ProductImage `gorm:"foreignKey:ProductID" json:"images,omitempty"`
// 商品属性字典外键(可空,公开页展示用)
OriginID *uint64 `json:"origin_id"`
ShelfLifeID *uint64 `json:"shelf_life_id"`
StorageID *uint64 `json:"storage_id"`
DescriptionDocID *uint64 `json:"description_doc_id"`
Category *ProductCategory `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
Images []ProductImage `gorm:"foreignKey:ProductID" json:"images,omitempty"`
Origin *ProductOriginOption `gorm:"foreignKey:OriginID" json:"origin,omitempty"`
ShelfLife *ProductShelfLifeOption `gorm:"foreignKey:ShelfLifeID" json:"shelf_life,omitempty"`
Storage *ProductStorageOption `gorm:"foreignKey:StorageID" json:"storage,omitempty"`
DescriptionDoc *ProductDescriptionDoc `gorm:"foreignKey:DescriptionDocID" json:"description_doc,omitempty"`
}
+34
View File
@@ -0,0 +1,34 @@
package model
// ProductOriginOption 产地字典(门店级)
type ProductOriginOption struct {
TenantBase
Code string `gorm:"size:50" json:"code"`
Name string `gorm:"size:200;not null" json:"name"`
Remark string `gorm:"size:500" json:"remark"`
}
// ProductShelfLifeOption 保质期字典(门店级)
type ProductShelfLifeOption struct {
TenantBase
Code string `gorm:"size:50" json:"code"`
Name string `gorm:"size:200;not null" json:"name"` // 如「无限期(适饮)」「24个月」
Remark string `gorm:"size:500" json:"remark"`
}
// ProductStorageOption 储存方式字典(门店级)
type ProductStorageOption struct {
TenantBase
Code string `gorm:"size:50" json:"code"`
Name string `gorm:"size:200;not null" json:"name"` // 如「阴凉干燥、避光保存」
Remark string `gorm:"size:500" json:"remark"`
}
// ProductDescriptionDoc 商品描述文档字典(门店级)
// Title = 酒类型(如「酱香型白酒」),Content = 商品介绍正文
type ProductDescriptionDoc struct {
TenantBase
Title string `gorm:"size:200;not null" json:"title"`
Content string `gorm:"type:text" json:"content"`
Remark string `gorm:"size:500" json:"remark"`
}
+2
View File
@@ -4,11 +4,13 @@ type Shop struct {
Base
Name string `gorm:"size:100;not null" json:"name"`
Code string `gorm:"size:50;uniqueIndex" json:"code"`
Description string `gorm:"type:text" json:"description"`
Address string `gorm:"size:255" json:"address"`
Phone string `gorm:"size:30" json:"phone"`
BusinessHours string `gorm:"size:100" json:"business_hours"`
ManagerName string `gorm:"size:50" json:"manager_name"`
LogoURL string `gorm:"column:logo_url;size:500" json:"logo_url"`
WechatID string `gorm:"size:100" json:"wechat_id"`
BusinessLicense string `gorm:"size:500" json:"business_license"`
ShopPhotos JSON `gorm:"type:json" json:"shop_photos,omitempty"`
CustomFields JSON `gorm:"type:json" json:"custom_fields,omitempty"`
+37
View File
@@ -27,12 +27,15 @@ func Setup(r *gin.Engine, db *gorm.DB) {
importH := handler.NewImportHandler(db)
userH := handler.NewUserHandler(db)
productOptH := handler.NewProductOptionHandler(db)
productAttrH := handler.NewProductAttrHandler(db)
productImageH := handler.NewProductImageHandler(db)
financeH := handler.NewFinanceHandler(db)
numberRuleH := handler.NewNumberRuleHandler(db)
publicH := handler.NewPublicHandler(db)
adminH := handler.NewAdminHandler(db)
shopH := handler.NewShopHandler(db)
errorReportH := handler.NewErrorReportHandler(db)
feedbackH := handler.NewFeedbackHandler(db)
// 健康检查(无需认证,用于前端连通性探测)
r.GET("/health", func(c *gin.Context) {
@@ -55,7 +58,10 @@ func Setup(r *gin.Engine, db *gorm.DB) {
public := v1.Group("/public")
{
public.GET("/products/:public_id", publicH.GetProduct)
public.GET("/shops/:shop_code/products", publicH.ListShopProducts)
public.GET("/release", publicH.GetRelease)
public.POST("/errors", errorReportH.Submit)
public.POST("/register", authH.Register)
}
// 需要 JWT 的路由(ReadOnly 中间件:只读用户不可执行写操作)
@@ -168,6 +174,13 @@ func Setup(r *gin.Engine, db *gorm.DB) {
shop.POST("/logo", middleware.AdminOnly(), shopH.UploadLogo)
}
// 意见反馈(文字 + 附图,直接提交后台)
feedback := api.Group("/feedback")
{
feedback.POST("", feedbackH.Submit)
feedback.POST("/images", feedbackH.UploadImage)
}
// 编号规则
numberRules := api.Group("/number-rules")
{
@@ -206,6 +219,27 @@ func Setup(r *gin.Engine, db *gorm.DB) {
opts.POST("/specs", productOptH.CreateSpec)
opts.PUT("/specs/:id", productOptH.UpdateSpec)
opts.DELETE("/specs/:id", productOptH.DeleteSpec)
// 商品属性字典(产地/保质期/储存方式/描述文档)
opts.GET("/origins", productAttrH.ListOrigins)
opts.POST("/origins", productAttrH.CreateOrigin)
opts.PUT("/origins/:id", productAttrH.UpdateOrigin)
opts.DELETE("/origins/:id", productAttrH.DeleteOrigin)
opts.GET("/shelf-lives", productAttrH.ListShelfLives)
opts.POST("/shelf-lives", productAttrH.CreateShelfLife)
opts.PUT("/shelf-lives/:id", productAttrH.UpdateShelfLife)
opts.DELETE("/shelf-lives/:id", productAttrH.DeleteShelfLife)
opts.GET("/storages", productAttrH.ListStorages)
opts.POST("/storages", productAttrH.CreateStorage)
opts.PUT("/storages/:id", productAttrH.UpdateStorage)
opts.DELETE("/storages/:id", productAttrH.DeleteStorage)
opts.GET("/description-docs", productAttrH.ListDescriptionDocs)
opts.POST("/description-docs", productAttrH.CreateDescriptionDoc)
opts.PUT("/description-docs/:id", productAttrH.UpdateDescriptionDoc)
opts.DELETE("/description-docs/:id", productAttrH.DeleteDescriptionDoc)
}
// 超级管理员专属
@@ -213,6 +247,9 @@ func Setup(r *gin.Engine, db *gorm.DB) {
superAdmin.Use(middleware.SuperAdminOnly())
{
superAdmin.POST("/clear-data", adminH.ClearData)
superAdmin.GET("/errors", errorReportH.List)
superAdmin.GET("/feedback", feedbackH.List)
superAdmin.PATCH("/feedback/:id", feedbackH.UpdateStatus)
}
}
}
+72
View File
@@ -2,9 +2,11 @@ package service
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
@@ -61,6 +63,76 @@ func (s *AuthService) Login(shopCode, username, password string) (*TokenPair, *m
return pair, &user, nil
}
// RegisterInput 注册新门店所需参数
type RegisterInput struct {
ShopName string `json:"shop_name" binding:"required"`
Address string `json:"address" binding:"required"`
ManagerName string `json:"manager_name" binding:"required"`
Phone string `json:"phone" binding:"required"`
Description string `json:"description"`
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
// RegisterResult 注册成功后返回的数据
type RegisterResult struct {
ShopCode string `json:"shop_code"`
ShopName string `json:"shop_name"`
Username string `json:"username"`
}
// Register 自助注册新门店(公开接口,无需认证)
func (s *AuthService) Register(in RegisterInput) (*RegisterResult, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
var result RegisterResult
err = s.db.Transaction(func(tx *gorm.DB) error {
// 先用 UUID 占位创建门店,得到真实 ID
shop := model.Shop{
Name: in.ShopName,
Code: uuid.New().String(),
Address: in.Address,
Phone: in.Phone,
ManagerName: in.ManagerName,
Description: in.Description,
}
if err := tx.Create(&shop).Error; err != nil {
return err
}
// 用自增 ID 生成正式门店编码
shop.Code = fmt.Sprintf("S%06d", shop.ID)
if err := tx.Model(&shop).Update("code", shop.Code).Error; err != nil {
return err
}
// 创建管理员用户
user := model.User{
ShopID: shop.ID,
Username: in.Username,
PasswordHash: string(hash),
RealName: in.ManagerName,
Phone: in.Phone,
Role: "superadmin",
IsActive: true,
}
if err := tx.Create(&user).Error; err != nil {
return err
}
result = RegisterResult{
ShopCode: shop.Code,
ShopName: shop.Name,
Username: user.Username,
}
return nil
})
return &result, err
}
// HashPassword 生成 bcrypt 哈希
func HashPassword(plain string) (string, error) {
b, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
+48
View File
@@ -0,0 +1,48 @@
package util
import (
"strings"
gp "github.com/mozillazg/go-pinyin"
)
var (
fullArgs = func() gp.Args {
a := gp.NewArgs()
a.Style = gp.Normal
a.Fallback = func(r rune, _ gp.Args) []string {
return []string{strings.ToLower(string(r))}
}
return a
}()
initialArgs = func() gp.Args {
a := gp.NewArgs()
a.Style = gp.FirstLetter
a.Fallback = func(r rune, _ gp.Args) []string {
s := strings.ToLower(string(r))
if s == "" {
return nil
}
return []string{string(s[0])}
}
return a
}()
)
// ToPinyin converts a Chinese name to full pinyin and initials.
// Non-Chinese characters are kept as-is (lowercased).
// Example: "茅台酒" → ("maotaijiu", "mtj")
func ToPinyin(s string) (full, initials string) {
var fb, ib strings.Builder
for _, row := range gp.Pinyin(s, fullArgs) {
if len(row) > 0 {
fb.WriteString(row[0])
}
}
for _, row := range gp.Pinyin(s, initialArgs) {
if len(row) > 0 {
ib.WriteString(row[0])
}
}
return fb.String(), ib.String()
}
+26
View File
@@ -12,6 +12,7 @@ import (
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/internal/router"
"github.com/wangjia/jiu/backend/internal/util"
)
func main() {
@@ -27,6 +28,9 @@ func main() {
// 自动迁移(GORM AutoMigrate 只增不删,生产安全)
autoMigrate(db)
// 回填存量商品的拼音索引(一次性,已有值的跳过)
backfillPinyin(db)
// 启动 Gin
gin.SetMode(config.C.Server.Mode)
r := gin.New()
@@ -103,10 +107,32 @@ func autoMigrate(db *gorm.DB) {
&model.ProductNameOption{},
&model.ProductSeriesOption{},
&model.ProductSpecOption{},
&model.ProductOriginOption{},
&model.ProductShelfLifeOption{},
&model.ProductStorageOption{},
&model.ProductDescriptionDoc{},
&model.ProductImage{},
&model.ErrorReport{},
&model.Feedback{},
)
if err != nil {
log.Fatalf("auto migrate failed: %v", err)
}
log.Println("AutoMigrate completed")
}
func backfillPinyin(db *gorm.DB) {
var products []model.Product
db.Where("name_pinyin = '' OR name_pinyin IS NULL").Find(&products)
if len(products) == 0 {
return
}
for i := range products {
full, initials := util.ToPinyin(products[i].Name)
db.Model(&products[i]).Updates(map[string]interface{}{
"name_pinyin": full,
"name_initials": initials,
})
}
log.Printf("backfillPinyin: updated %d products", len(products))
}
+23
View File
@@ -17,6 +17,7 @@ CREATE TABLE IF NOT EXISTS `shops` (
`address` VARCHAR(255) DEFAULT NULL,
`phone` VARCHAR(30) DEFAULT NULL,
`manager_name` VARCHAR(50) DEFAULT NULL COMMENT '负责人',
`description` TEXT DEFAULT NULL COMMENT '店铺简介',
`logo_url` VARCHAR(500) DEFAULT '' COMMENT '门店 logo URL',
`business_license` VARCHAR(500) DEFAULT NULL COMMENT '营业执照照片URL',
`shop_photos` JSON DEFAULT NULL COMMENT '门店照片URL数组',
@@ -445,4 +446,26 @@ CREATE TABLE IF NOT EXISTS `product_spec_options` (
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品规格选项';
CREATE TABLE IF NOT EXISTS `feedbacks` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`shop_id` BIGINT UNSIGNED NOT NULL,
`shop_code` VARCHAR(50) DEFAULT NULL,
`user_id` BIGINT UNSIGNED DEFAULT NULL,
`username` VARCHAR(50) DEFAULT NULL,
`role` VARCHAR(20) DEFAULT NULL,
`type` VARCHAR(20) NOT NULL COMMENT 'bug / suggestion',
`content` TEXT,
`images` JSON,
`app_version` VARCHAR(30) DEFAULT NULL,
`platform` VARCHAR(20) DEFAULT NULL,
`client_ip` VARCHAR(60) DEFAULT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'new',
PRIMARY KEY (`id`),
KEY `idx_shop_id` (`shop_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_type` (`type`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户意见反馈';
SET FOREIGN_KEY_CHECKS = 1;
+48
View File
@@ -53,10 +53,12 @@ func SetupTestDB() *gorm.DB {
deleted_at DATETIME,
name TEXT NOT NULL,
code TEXT UNIQUE,
description TEXT,
address TEXT,
phone TEXT,
manager_name TEXT,
logo_url TEXT DEFAULT '',
wechat_id TEXT DEFAULT '',
business_license TEXT,
business_hours TEXT,
shop_photos TEXT,
@@ -121,6 +123,52 @@ func SetupTestDB() *gorm.DB {
min_stock INTEGER DEFAULT 0,
description TEXT,
custom_fields TEXT,
remark TEXT,
name_pinyin TEXT DEFAULT '',
name_initials TEXT DEFAULT '',
origin_id INTEGER,
shelf_life_id INTEGER,
storage_id INTEGER,
description_doc_id INTEGER
)`,
`CREATE TABLE IF NOT EXISTS product_origin_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
code TEXT,
name TEXT NOT NULL,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_shelf_life_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
code TEXT,
name TEXT NOT NULL,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_storage_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
code TEXT,
name TEXT NOT NULL,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS product_description_docs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME,
updated_at DATETIME,
deleted_at DATETIME,
shop_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT,
remark TEXT
)`,
`CREATE TABLE IF NOT EXISTS warehouses (
+6 -6
View File
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
- platform: web
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: ios
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
# User provided section
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+67
View File
@@ -0,0 +1,67 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
// 读取 release 签名配置(key.properties 不入库,由本地或 CI 生成)。
// 缺失时回退 debug 签名,保证本地 `flutter run --release` 与无密钥环境可用。
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
val hasReleaseSigning = keystorePropertiesFile.exists()
if (hasReleaseSigning) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.yanmei.jiu"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.yanmei.jiu"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
if (hasReleaseSigning) {
create("release") {
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
}
buildTypes {
release {
signingConfig = if (hasReleaseSigning) {
signingConfigs.getByName("release")
} else {
// 无正式密钥时回退 debug,便于本地构建/调试
signingConfigs.getByName("debug")
}
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,48 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- release 包走此主清单,必须显式声明联网权限;debug/profile 清单由 Flutter 模板自动添加,
release 不带会导致所有网络请求失败(登录页显示「服务不可达」)。 -->
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="岩美酒库"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.yanmei.jiu
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+6
View File
@@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+10
View File
@@ -0,0 +1,10 @@
{
"provider": "岩美技术有限公司",
"website": "https://jiu.yanmei.com",
"email": "yammy2023@163.com",
"phone": "",
"wechat": "",
"terms_url": "https://jiu.yanmei.com/terms/",
"privacy_url": "https://jiu.yanmei.com/privacy/",
"docs_url": "https://jiu.yanmei.com/docs/"
}
Binary file not shown.
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+43
View File
@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
+22
View File
@@ -0,0 +1,22 @@
PODS:
- Flutter (1.0.0)
- printing (1.0.0):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- printing (from `.symlinks/plugins/printing/ios`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
printing:
:path: ".symlinks/plugins/printing/ios"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
COCOAPODS: 1.16.2
+756
View File
@@ -0,0 +1,756 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3460B9FE6E275516D150453E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 086A871143ACEBCC07E71A24 /* Pods_Runner.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
CBD53FA90DB3F4E143A654CC /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
086A871143ACEBCC07E71A24 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
1EE111A264F501C30DD0A904 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
5A9159D36C6D9B7DF22F0FBF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9C64D02D6BED1C5F196D6192 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
0CE49F0D076B3E115DB6BF6D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
CBD53FA90DB3F4E143A654CC /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
3460B9FE6E275516D150453E /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
D5753B5E6E2CB7979B358C42 /* Pods */,
AA598329361F3C077ABDB4F2 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
AA598329361F3C077ABDB4F2 /* Frameworks */ = {
isa = PBXGroup;
children = (
086A871143ACEBCC07E71A24 /* Pods_Runner.framework */,
852943EE4176C906DE56A6AB /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
D5753B5E6E2CB7979B358C42 /* Pods */ = {
isa = PBXGroup;
children = (
9C64D02D6BED1C5F196D6192 /* Pods-Runner.debug.xcconfig */,
5A9159D36C6D9B7DF22F0FBF /* Pods-Runner.release.xcconfig */,
1EE111A264F501C30DD0A904 /* Pods-Runner.profile.xcconfig */,
08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */,
2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */,
F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
B21EFFE7BABA33CD7CB4E616 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
0CE49F0D076B3E115DB6BF6D /* Frameworks */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
3052FA6A8375458CBCA9F94D /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DB40772CF39C21BA0E695053 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3052FA6A8375458CBCA9F94D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
B21EFFE7BABA33CD7CB4E616 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
DB40772CF39C21BA0E695053 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 08787BE3C2097BFA0BE7AA92 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 2F6692A92756378556D65AC0 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = F4A0B9102B939B3B38F72001 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.yanmei.jiu;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,59 @@
{
"pins" : [
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,59 @@
{
"pins" : [
{
"identity" : "dkcamera",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKCamera",
"state" : {
"branch" : "master",
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
}
},
{
"identity" : "dkimagepickercontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKImagePickerController",
"state" : {
"branch" : "4.3.9",
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
}
},
{
"identity" : "dkphotogallery",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
"state" : {
"branch" : "master",
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
}
},
{
"identity" : "sdwebimage",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/SDWebImage",
"state" : {
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
"version" : "5.21.7"
}
},
{
"identity" : "swiftygif",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kirualex/SwiftyGif.git",
"state" : {
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
"version" : "5.4.5"
}
},
{
"identity" : "tocropviewcontroller",
"kind" : "remoteSourceControl",
"location" : "https://github.com/TimOliver/TOCropViewController",
"state" : {
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
"version" : "2.8.0"
}
}
],
"version" : 2
}
+16
View File
@@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Some files were not shown because too many files have changed in this diff Show More