Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0a74f3607 | |||
| 543bd2712f | |||
| fab7bd21dd | |||
| 3b2ebc0202 | |||
| ff32c8a845 | |||
| af64e29459 | |||
| f788a7ab74 | |||
| 8a87d4a82c | |||
| 0d17533120 | |||
| 4e64e5eb47 | |||
| 1ec1d4209a | |||
| 22f07ed805 | |||
| 45b6ec832b | |||
| ba127826b2 | |||
| 0e2a4086e1 | |||
| 99f34a6223 | |||
| ce92d619ed | |||
| 22a31f6eb8 | |||
| 1c901432a6 | |||
| e124ed7c65 | |||
| 46304ab961 | |||
| d1260e8258 | |||
| 5b980ec58b | |||
| 2fc5776435 | |||
| bc252d1fcb | |||
| c0001f08c2 | |||
| 46ef7db480 | |||
| 0c96d5e325 | |||
| ced407ea87 | |||
| e550b73d6d | |||
| fa02fdbfd8 | |||
| 211fafc7e1 | |||
| 5278bea940 | |||
| 0bc3f4b702 | |||
| cfeb8f0bba | |||
| 480ce836bb | |||
| 94f0fb2095 | |||
| 36e5677b7c | |||
| 7a1d8465e5 | |||
| dd09f1a526 | |||
| 53a52cc530 | |||
| 337a358d54 | |||
| c98a90472b | |||
| 207a9a8343 | |||
| d8c0bd74d2 | |||
| 056ae914ce | |||
| 022443476c | |||
| 93f4f87e44 | |||
| 20e7d07be3 | |||
| 1834740676 | |||
| 90a194fc00 | |||
| fba8b0a917 | |||
| c001f0e8e6 | |||
| f039bd0c61 | |||
| f4beb90769 | |||
| 6f0b273950 | |||
| b43601b4fd | |||
| 3aa63d6575 | |||
| a3f9f091b4 | |||
| b3f5a0f083 | |||
| 0af92e1732 | |||
| ea5526944b | |||
| c2f8bda4f3 | |||
| 7f6584ab64 | |||
| 23de243598 | |||
| 65e2021138 | |||
| d94c6b109b | |||
| 75e3b934bc | |||
| ec7596e272 | |||
| 9fc379f33a | |||
| ee826eca7f | |||
| 51863dcbd3 | |||
| eda7d37b64 | |||
| 471b980179 | |||
| 3d1fc0d4ba | |||
| b6223b3816 | |||
| 117cec431b | |||
| d8295d14da | |||
| 5282eb57c9 | |||
| f59bdf056f | |||
| 01946282ed | |||
| 0d4c9cced2 | |||
| e9a6543a8b | |||
| 5f2248001d | |||
| 2dbc89b902 | |||
| 44ffedb56a | |||
| 93878511e5 | |||
| 503ee7372e | |||
| 62dc54fe1c |
@@ -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`)。
|
||||
任一失败则停止,指出失败的命令和原因。
|
||||
@@ -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: 纯内部)
|
||||
- 依赖升级、版本号 bump(chore: 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 通知。
|
||||
@@ -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)。
|
||||
@@ -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(共/未完成/已完成及分级列表)。
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
初始版本发布,完成核心库存管理功能。
|
||||
|
||||
@@ -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. 静态分析无 error(warning/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/CD(Forgejo,`.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()`
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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"})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
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>
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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/"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |