feat: 初始化 Wails + React + Go 桌面应用模板
- Wails v2.16.0 + React 19 + TypeScript + Vite 7 - 插件系统(build tag 控制编译,plugin.json 运行时配置) - 支付插件(授权、账号、订单、退款、账单) - 高并发基础设施(任务池、批量处理器、限流器、环形缓冲、管道) - Windows 系统托盘(Win32 API) - CodeGraph 索引 + .memory 记忆包 - 默认图标 assets/icon.png
This commit is contained in:
5
.codegraph/.gitignore
vendored
Normal file
5
.codegraph/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# CodeGraph data files — local to each machine, not for committing.
|
||||||
|
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||||
|
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
build/bin
|
||||||
|
node_modules
|
||||||
|
frontend/dist
|
||||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
252
.memory/MEMORY.md
Normal file
252
.memory/MEMORY.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
# 项目记忆包 — go_win_template
|
||||||
|
|
||||||
|
> 此目录为 AI 辅助开发项目的记忆索引。
|
||||||
|
> 核心工具:**CodeGraph**(代码知识图谱),用于快速检索项目符号和调用关系。
|
||||||
|
> 当 `.codegraph/` 目录存在时,优先使用 CodeGraph 探索代码,而非 grep/find。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 项目概述
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
|------|-----|
|
||||||
|
| 项目类型 | Wails v2 + React + TypeScript 桌面应用 |
|
||||||
|
| 目标平台 | Windows (amd64) |
|
||||||
|
| 框架版本 | Wails v2.16.0 |
|
||||||
|
| 前端框架 | React 19 + Vite 7 + TypeScript 5 |
|
||||||
|
| Go 版本 | Go 1.25.0 |
|
||||||
|
|
||||||
|
### 技术栈
|
||||||
|
- **后端**: Go 原生,Wails IPC (进程内通信,不走 HTTP)
|
||||||
|
- **前端**: React + TypeScript + Vite
|
||||||
|
- **系统托盘**: Windows Win32 API (`shell32.dll::Shell_NotifyIconA`)
|
||||||
|
- **插件系统**: build tag 控制编译,`config/plugin.json` 运行时配置
|
||||||
|
- **支付模块**: HTTP 客户端 + 自动 token 续期 + HMAC 签名
|
||||||
|
- **高并发**: 任务池、批量处理器、限流器、环形缓冲、管道
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🕸️ CodeGraph 使用指南
|
||||||
|
|
||||||
|
### 安装状态
|
||||||
|
```
|
||||||
|
已全局安装: @colbymchenry/codegraph@1.6.0
|
||||||
|
项目索引: .codegraph/ (25 files, 431 nodes, 947 edges)
|
||||||
|
MCP 集成: opencode (已配置)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 初始化/重建索引
|
||||||
|
codegraph init [path]
|
||||||
|
|
||||||
|
# 查看项目文件结构
|
||||||
|
codegraph files
|
||||||
|
|
||||||
|
# 查看索引状态和统计
|
||||||
|
codegraph status
|
||||||
|
|
||||||
|
# 搜索符号
|
||||||
|
codegraph query <search_term>
|
||||||
|
|
||||||
|
# 探索代码区域(源码 + 调用路径)—— 最常用的命令
|
||||||
|
codegraph explore "<topic>"
|
||||||
|
|
||||||
|
# 构建任务上下文(相关符号、关系、代码块)
|
||||||
|
codegraph context "<task>"
|
||||||
|
|
||||||
|
# 查看单个符号的源码和调用链
|
||||||
|
codegraph node <symbol_name>
|
||||||
|
|
||||||
|
# 查找谁调用了某个函数
|
||||||
|
codegraph callers <symbol>
|
||||||
|
|
||||||
|
# 查找某个函数调用了什么
|
||||||
|
codegraph callees <symbol>
|
||||||
|
|
||||||
|
# 分析修改某符号的影响范围
|
||||||
|
codegraph impact <symbol>
|
||||||
|
|
||||||
|
# 查找受影响的测试文件
|
||||||
|
codegraph affected [files...]
|
||||||
|
|
||||||
|
# 更新索引(增量同步)
|
||||||
|
codegraph sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### 典型工作流
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发新功能前,先构建上下文
|
||||||
|
codegraph context "实现新的插件功能"
|
||||||
|
|
||||||
|
# 查看关键入口函数的完整调用链
|
||||||
|
codegraph node "App.Startup"
|
||||||
|
|
||||||
|
# 探索某个模块的架构
|
||||||
|
codegraph explore "payment service architecture"
|
||||||
|
|
||||||
|
# 修改代码前确认影响范围
|
||||||
|
codegraph impact "CreateUser"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在 AI 对话中的使用
|
||||||
|
|
||||||
|
当需要理解或修改代码时,**优先使用 CodeGraph 而不是 grep**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ❌ 不要这样(慢且碎片化)
|
||||||
|
grep -r "FunctionName" --include="*.go" .
|
||||||
|
|
||||||
|
# ✅ 应该这样(精准且带上下文)
|
||||||
|
codegraph explore "FunctionName"
|
||||||
|
codegraph node "FunctionName"
|
||||||
|
codegraph callees "FunctionName"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
go_win_template/
|
||||||
|
├── main.go # 入口:wails.Run + 插件启动
|
||||||
|
├── app.go # IPC 接口层:基础业务 + 支付接口
|
||||||
|
├── wails.json # Wails 配置
|
||||||
|
├── go.mod / go.sum
|
||||||
|
│
|
||||||
|
├── config/
|
||||||
|
│ └── plugin.json # 插件开关配置(运行时生效)
|
||||||
|
│
|
||||||
|
├── assets/icon.png # 应用图标
|
||||||
|
├── trayicons/icon.png # 托盘图标(Wails 标准路径)
|
||||||
|
│
|
||||||
|
├── internal/
|
||||||
|
│ ├── app/app.go # 全局上下文单例
|
||||||
|
│ ├── bus/bus.go # 事件总线(模块间解耦)
|
||||||
|
│ ├── cache/cache.go # TTL 内存缓存
|
||||||
|
│ ├── service/concurrent.go # 高并发:BatchWorker、RateLimiter、RingBuffer、Pipeline
|
||||||
|
│ ├── worker/pool.go # 任务并发池(固定工作协程)
|
||||||
|
│ ├── payment/
|
||||||
|
│ │ ├── client.go # HTTP 客户端:自动 token 续期 + HMAC 签名
|
||||||
|
│ │ └── service.go # 支付业务层(登录、查订单、退款、账单)
|
||||||
|
│ ├── plugins/
|
||||||
|
│ │ ├── plugins.go # 插件接口 + 全局注册中心
|
||||||
|
│ │ ├── loader.go # 配置文件加载器(支持环境变量覆盖)
|
||||||
|
│ │ ├── iconfont_impl.go # build tag: iconfont
|
||||||
|
│ │ ├── concurrent_impl.go # build tag: concurrent
|
||||||
|
│ │ ├── ai_impl.go # build tag: ai
|
||||||
|
│ │ └── payment_impl.go # build tag: payment
|
||||||
|
│ └── tray/win/tray.go # Windows 系统托盘(Win32 API)
|
||||||
|
│
|
||||||
|
├── frontend/ # React + TypeScript + Vite
|
||||||
|
│ ├── src/App.tsx # 主页面(用户管理示例)
|
||||||
|
│ ├── src/main.tsx # React 入口
|
||||||
|
│ ├── wailsjs/ # Go → JS 绑定(自动生成)
|
||||||
|
│ └── vite.config.ts
|
||||||
|
│
|
||||||
|
├── .codegraph/ # CodeGraph 索引(自动生成,勿手动编辑)
|
||||||
|
│ └── codegraph.db
|
||||||
|
│
|
||||||
|
└── .memory/ # 本记忆包
|
||||||
|
└── MEMORY.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 插件系统
|
||||||
|
|
||||||
|
### 插件列表
|
||||||
|
|
||||||
|
| 插件名 | Build Tag | 默认状态 | 功能 |
|
||||||
|
|--------|-----------|----------|------|
|
||||||
|
| `concurrent` | `-tags concurrent` | ✅ 启用 | 高并发工具集 |
|
||||||
|
| `payment` | `-tags payment` | ✅ 启用 | 支付授权与订单管理 |
|
||||||
|
| `iconfont` | `-tags iconfont` | ❌ 禁用 | 阿里IconFont图标库 |
|
||||||
|
| `ai` | `-tags ai` | ❌ 禁用 | AI大模型集成 |
|
||||||
|
|
||||||
|
### 运行时配置(无需重新编译)
|
||||||
|
|
||||||
|
编辑 `config/plugin.json` 即可切换插件开关:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"payment": {
|
||||||
|
"enabled": true,
|
||||||
|
"params": { "app_id": "...", "api_key": "...", "base_url": "..." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 编译时启用(完全排除二进制)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 仅基础功能
|
||||||
|
wails build
|
||||||
|
|
||||||
|
# 加支付插件
|
||||||
|
wails build -tags payment
|
||||||
|
|
||||||
|
# 加 AI 插件
|
||||||
|
wails build -tags ai
|
||||||
|
|
||||||
|
# 全部启用
|
||||||
|
wails build -tags "payment,ai,iconfont"
|
||||||
|
|
||||||
|
# 环境变量覆盖
|
||||||
|
PLUGIN_AI=true wails build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 支付接口(IPC — 前端可调用)
|
||||||
|
|
||||||
|
| Go 方法 | 说明 |
|
||||||
|
|---------|------|
|
||||||
|
| `PayLogin()` | 登录授权,获取 access_token |
|
||||||
|
| `PayCheckAuth()` | 查询授权状态 |
|
||||||
|
| `PayRefreshToken()` | 主动刷新令牌 |
|
||||||
|
| `PayGetAccountInfo()` | 获取账号信息 |
|
||||||
|
| `PayQueryOrder(orderID)` | 查订单 |
|
||||||
|
| `PayQueryOrderByOutNo(outTradeNo)` | 按商户号查订单 |
|
||||||
|
| `PayRequestRefund(orderID, amount, reason)` | 申请退款 |
|
||||||
|
| `PayGetBill(page, pageSize)` | 分页账单 |
|
||||||
|
| `PayGetBillByDate(start, end)` | 按日期查账单 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 开发命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式(热更新,自动选端口)
|
||||||
|
wails dev
|
||||||
|
|
||||||
|
# 构建 Windows exe
|
||||||
|
wails build
|
||||||
|
|
||||||
|
# 重新生成前端 JS 绑定(新增 IPC 方法后执行)
|
||||||
|
wails generate module
|
||||||
|
|
||||||
|
# 更新 CodeGraph 索引
|
||||||
|
codegraph sync
|
||||||
|
|
||||||
|
# 查看项目结构索引
|
||||||
|
codegraph files
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 重要注意事项
|
||||||
|
|
||||||
|
1. **端口冲突**: Wails dev 模式 Vite 自动选可用端口(`strictPort: false`),多实例不冲突
|
||||||
|
2. **生产模式**: 静态资源打包进 exe,无端口暴露,用户无法直接访问端口
|
||||||
|
3. **托盘图标**: Wails v2 标准路径 `trayicons/icon.png`,名称即 ID(`Image: "icon"`)
|
||||||
|
4. **支付插件**: 需在 `config/plugin.json` 的 `params` 中配置 `app_id`、`api_key`、`base_url`
|
||||||
|
5. **CodeGraph**: 索引存储在 `.codegraph/codegraph.db`,修改代码后运行 `codegraph sync` 同步
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*记忆包创建: 2026-09-17*
|
||||||
|
*CodeGraph 索引: 25 files, 431 nodes, 947 edges*
|
||||||
19
README.md
Normal file
19
README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# README
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
This is the official Wails React-TS template.
|
||||||
|
|
||||||
|
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||||
|
here: https://wails.io/docs/reference/project-config
|
||||||
|
|
||||||
|
## Live Development
|
||||||
|
|
||||||
|
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||||
|
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||||
|
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||||
|
to this in your browser, and you can call your Go code from devtools.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
To build a redistributable, production mode package, use `wails build`.
|
||||||
322
app.go
Normal file
322
app.go
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go_win_template/internal/bus"
|
||||||
|
"go_win_template/internal/cache"
|
||||||
|
"go_win_template/internal/plugins"
|
||||||
|
"go_win_template/internal/service"
|
||||||
|
"go_win_template/internal/worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App 主应用结构体
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
users []User
|
||||||
|
nextID atomic.Int64
|
||||||
|
userCache *cache.Cache
|
||||||
|
eventBus *bus.EventBus
|
||||||
|
batchWorker *service.BatchWorker
|
||||||
|
taskPool *worker.Pool
|
||||||
|
requestCount atomic.Int64
|
||||||
|
paymentSvc paymentService
|
||||||
|
}
|
||||||
|
|
||||||
|
// paymentService 支付服务接口(定义在 app 层,不与 payment 包直接耦合)
|
||||||
|
// payment 插件在启动时通过类型断言注入此接口
|
||||||
|
type paymentService interface {
|
||||||
|
PayLogin() (map[string]any, error)
|
||||||
|
PayCheckAuth() (map[string]any, error)
|
||||||
|
PayRefresh() (map[string]any, error)
|
||||||
|
PayGetAccountInfo() (map[string]any, error)
|
||||||
|
PayQueryOrder(orderID string) (map[string]any, error)
|
||||||
|
PayQueryOrderByOutNo(outTradeNo string) (map[string]any, error)
|
||||||
|
PayRequestRefund(req map[string]any) (map[string]any, error)
|
||||||
|
PayGetBill(page, pageSize int) (map[string]any, error)
|
||||||
|
PayGetBillByDate(startDate, endDate string) (map[string]any, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response 统一响应
|
||||||
|
type Response struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User 用户模型
|
||||||
|
type User struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// 初始化
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{
|
||||||
|
users: make([]User, 0),
|
||||||
|
userCache: cache.NewCache(cache.WithTTL(10*time.Minute), cache.WithCleanupInterval(1*time.Minute)),
|
||||||
|
eventBus: bus.NewEventBus(),
|
||||||
|
batchWorker: service.NewBatchWorker(8),
|
||||||
|
taskPool: worker.NewPool(worker.Options{Workers: 4, BufSize: 500}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Startup(ctx context.Context) {
|
||||||
|
a.ctx = ctx
|
||||||
|
fmt.Println("[app] startup complete")
|
||||||
|
|
||||||
|
// 注入支付服务
|
||||||
|
a.injectPaymentService()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) injectPaymentService() {
|
||||||
|
payP := plugins.Get("payment")
|
||||||
|
if payP == nil {
|
||||||
|
fmt.Println("[app] payment plugin not loaded (use -tags payment to enable)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if getter, ok := payP.(interface{ GetPaymentService() any }); ok {
|
||||||
|
if svc, ok := getter.GetPaymentService().(paymentService); ok {
|
||||||
|
a.paymentSvc = svc
|
||||||
|
fmt.Println("[app] payment service injected successfully")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("[app] payment plugin found but service type mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// IPC 接口 — 基础功能
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (a *App) GetUserList() []User {
|
||||||
|
a.requestCount.Add(1)
|
||||||
|
if items, ok := a.userCache.Get("user_list"); ok {
|
||||||
|
if cached, ok := items.([]User); ok {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) CreateUser(name, email string) *Response {
|
||||||
|
if name == "" || email == "" {
|
||||||
|
return &Response{Code: 400, Message: "名称和邮箱不能为空"}
|
||||||
|
}
|
||||||
|
id := a.nextID.Add(1)
|
||||||
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
user := User{ID: id, Name: name, Email: email, CreatedAt: now}
|
||||||
|
a.users = append(a.users, user)
|
||||||
|
a.userCache.Set("user_list", a.users)
|
||||||
|
a.eventBus.PublishAsync("user:created", user)
|
||||||
|
return &Response{Code: 0, Message: "success", Data: user}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ClearUsers() int {
|
||||||
|
count := len(a.users)
|
||||||
|
a.users = make([]User, 0)
|
||||||
|
a.userCache.Delete("user_list")
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) Stats() map[string]interface{} {
|
||||||
|
running, total, failed := a.taskPool.Stats()
|
||||||
|
br, bt, bs, bf := a.batchWorker.Stats()
|
||||||
|
return map[string]interface{}{
|
||||||
|
"total_users": len(a.users),
|
||||||
|
"cache_items": a.userCache.Len(),
|
||||||
|
"requests": a.requestCount.Load(),
|
||||||
|
"pool_running": running,
|
||||||
|
"pool_total": total,
|
||||||
|
"pool_failed": failed,
|
||||||
|
"batch_running": br,
|
||||||
|
"batch_total": bt,
|
||||||
|
"batch_success": bs,
|
||||||
|
"batch_failed": bf,
|
||||||
|
"plugins": plugins.List(),
|
||||||
|
"plugin_count": plugins.Count(),
|
||||||
|
"payment_loaded": a.paymentSvc != nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// IPC 接口 — 支付功能
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (a *App) PayLogin() *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载,使用 -tags payment 构建或检查 plugin.json"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayLogin()
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PayCheckAuth() *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayCheckAuth()
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PayRefreshToken() *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayRefresh()
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PayGetAccountInfo() *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayGetAccountInfo()
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PayQueryOrder(orderID string) *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayQueryOrder(orderID)
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PayQueryOrderByOutNo(outTradeNo string) *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayQueryOrderByOutNo(outTradeNo)
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PayRequestRefund(orderID string, refundAmt float64, reason string) *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayRequestRefund(map[string]any{
|
||||||
|
"order_id": orderID,
|
||||||
|
"refund_amt": refundAmt,
|
||||||
|
"reason": reason,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PayGetBill(page, pageSize int) *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayGetBill(page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PayGetBillByDate(startDate, endDate string) *Response {
|
||||||
|
if a.paymentSvc == nil {
|
||||||
|
return &Response{Code: 503, Message: "支付插件未加载"}
|
||||||
|
}
|
||||||
|
data, err := a.paymentSvc.PayGetBillByDate(startDate, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return &Response{Code: 500, Message: err.Error()}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: asString(data, "message"), Data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// 高并发批量处理
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (a *App) BatchCreateUsers(names, emails []string) *Response {
|
||||||
|
if len(names) != len(emails) || len(names) == 0 {
|
||||||
|
return &Response{Code: 400, Message: "names 和 emails 长度需一致且不为空"}
|
||||||
|
}
|
||||||
|
results := a.batchWorker.ProcessConcurrent(a.ctx, createBatchItems(names, emails), func(ctx context.Context, item any) (any, error) {
|
||||||
|
bp := item.(batchItem)
|
||||||
|
id := a.nextID.Add(1)
|
||||||
|
user := User{ID: id, Name: bp.name, Email: bp.email, CreatedAt: time.Now().Format("2006-01-02 15:04:05")}
|
||||||
|
a.users = append(a.users, user)
|
||||||
|
return user, nil
|
||||||
|
})
|
||||||
|
a.userCache.Set("user_list", a.users)
|
||||||
|
a.eventBus.PublishAsync("users:batch_created", results)
|
||||||
|
return &Response{
|
||||||
|
Code: 0,
|
||||||
|
Message: fmt.Sprintf("成功 %d 条,失败 %d 条", len(results.Success), len(results.Fail)),
|
||||||
|
Data: map[string]any{"success": len(results.Success), "failed": len(results.Fail)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SubmitTask(taskName, taskData string) *Response {
|
||||||
|
job := &exampleJob{name: taskName, data: taskData}
|
||||||
|
if !a.taskPool.Submit(job) {
|
||||||
|
return &Response{Code: 503, Message: "任务队列已满,请稍后重试"}
|
||||||
|
}
|
||||||
|
return &Response{Code: 0, Message: "任务已入队", Data: map[string]any{"task": taskName}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// 内部类型
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
type batchItem struct{ name, email string }
|
||||||
|
|
||||||
|
func createBatchItems(names, emails []string) []any {
|
||||||
|
items := make([]any, len(names))
|
||||||
|
for i := range names {
|
||||||
|
items[i] = batchItem{name: names[i], email: emails[i]}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
type exampleJob struct{ name, data string }
|
||||||
|
|
||||||
|
func (j *exampleJob) JobID() string { return j.name }
|
||||||
|
|
||||||
|
func (j *exampleJob) Execute(ctx context.Context) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// asString 安全地从 map 中提取字符串字段
|
||||||
|
func asString(m map[string]any, key string) string {
|
||||||
|
if v, ok := m[key].(string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 477 B |
35
build/README.md
Normal file
35
build/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Build Directory
|
||||||
|
|
||||||
|
The build directory is used to house all the build files and assets for your application.
|
||||||
|
|
||||||
|
The structure is:
|
||||||
|
|
||||||
|
* bin - Output directory
|
||||||
|
* darwin - macOS specific files
|
||||||
|
* windows - Windows specific files
|
||||||
|
|
||||||
|
## Mac
|
||||||
|
|
||||||
|
The `darwin` directory holds files specific to Mac builds.
|
||||||
|
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||||
|
and
|
||||||
|
build with `wails build`.
|
||||||
|
|
||||||
|
The directory contains the following files:
|
||||||
|
|
||||||
|
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||||
|
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||||
|
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||||
|
build with `wails build`.
|
||||||
|
|
||||||
|
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||||
|
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||||
|
will be created using the `appicon.png` file in the build directory.
|
||||||
|
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||||
|
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||||
|
as well as the application itself (right click the exe -> properties -> details)
|
||||||
|
- `wails.exe.manifest` - The main application manifest file.
|
||||||
BIN
build/appicon.png
Normal file
BIN
build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
68
build/darwin/Info.dev.plist
Normal file
68
build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.OutputFilename}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{safeBundleID .Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
63
build/darwin/Info.plist
Normal file
63
build/darwin/Info.plist
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>{{.Info.ProductName}}</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>{{.OutputFilename}}</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.wails.{{safeBundleID .Name}}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleGetInfoString</key>
|
||||||
|
<string>{{.Info.Comments}}</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>{{.Info.ProductVersion}}</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>iconfile</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.13.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<string>true</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>{{.Info.Copyright}}</string>
|
||||||
|
{{if .Info.FileAssociations}}
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Ext}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>{{.Name}}</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>{{.IconName}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
{{if .Info.Protocols}}
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.wails.{{.Scheme}}</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>{{.Scheme}}</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>{{.Role}}</string>
|
||||||
|
</dict>
|
||||||
|
{{end}}
|
||||||
|
</array>
|
||||||
|
{{end}}
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
build/windows/icon.ico
Normal file
BIN
build/windows/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
15
build/windows/info.json
Normal file
15
build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fixed": {
|
||||||
|
"file_version": "{{.Info.ProductVersion}}"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"0000": {
|
||||||
|
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||||
|
"CompanyName": "{{.Info.CompanyName}}",
|
||||||
|
"FileDescription": "{{.Info.ProductName}}",
|
||||||
|
"LegalCopyright": "{{.Info.Copyright}}",
|
||||||
|
"ProductName": "{{.Info.ProductName}}",
|
||||||
|
"Comments": "{{.Info.Comments}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
build/windows/installer/project.nsi
Normal file
122
build/windows/installer/project.nsi
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
Unicode true
|
||||||
|
|
||||||
|
####
|
||||||
|
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||||
|
## mentioned underneath.
|
||||||
|
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||||
|
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||||
|
## from outside of Wails for debugging and development of the installer.
|
||||||
|
##
|
||||||
|
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||||
|
## > wails build --target windows/amd64 --nsis
|
||||||
|
## Then you can call makensis on this file with specifying the path to your binary:
|
||||||
|
## For a AMD64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a ARM64 only installer:
|
||||||
|
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||||
|
## For a installer with both architectures:
|
||||||
|
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||||
|
####
|
||||||
|
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||||
|
####
|
||||||
|
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||||
|
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||||
|
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||||
|
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||||
|
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||||
|
###
|
||||||
|
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||||
|
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
####
|
||||||
|
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||||
|
####
|
||||||
|
## Include the wails tools
|
||||||
|
####
|
||||||
|
!include "wails_tools.nsh"
|
||||||
|
|
||||||
|
# The version information for this two must consist of 4 parts
|
||||||
|
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||||
|
|
||||||
|
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||||
|
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||||
|
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||||
|
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||||
|
|
||||||
|
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||||
|
ManifestDPIAware true
|
||||||
|
|
||||||
|
!include "MUI.nsh"
|
||||||
|
|
||||||
|
!define MUI_ICON "..\icon.ico"
|
||||||
|
!define MUI_UNICON "..\icon.ico"
|
||||||
|
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||||
|
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||||
|
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||||
|
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||||
|
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||||
|
|
||||||
|
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||||
|
#!uninstfinalize 'signtool --file "%1"'
|
||||||
|
#!finalize 'signtool --file "%1"'
|
||||||
|
|
||||||
|
Name "${INFO_PRODUCTNAME}"
|
||||||
|
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||||
|
!ifdef WAILS_INSTALL_SCOPE
|
||||||
|
!if "${WAILS_INSTALL_SCOPE}" == "user"
|
||||||
|
InstallDir "$LOCALAPPDATA\Programs\${INFO_PRODUCTNAME}"
|
||||||
|
!else
|
||||||
|
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}"
|
||||||
|
!endif # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||||
|
ShowInstDetails show # This will always show the installation details.
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
!insertmacro wails.checkArchitecture
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
!insertmacro wails.webview2runtime
|
||||||
|
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
|
||||||
|
!insertmacro wails.files
|
||||||
|
|
||||||
|
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
|
||||||
|
!insertmacro wails.associateFiles
|
||||||
|
!insertmacro wails.associateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.writeUninstaller
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "uninstall"
|
||||||
|
!insertmacro wails.setShellContext
|
||||||
|
|
||||||
|
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||||
|
|
||||||
|
RMDir /r $INSTDIR
|
||||||
|
|
||||||
|
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||||
|
|
||||||
|
!insertmacro wails.unassociateFiles
|
||||||
|
!insertmacro wails.unassociateCustomProtocols
|
||||||
|
|
||||||
|
!insertmacro wails.deleteUninstaller
|
||||||
|
SectionEnd
|
||||||
284
build/windows/installer/wails_tools.nsh
Normal file
284
build/windows/installer/wails_tools.nsh
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
# DO NOT EDIT - Generated automatically by `wails build`
|
||||||
|
|
||||||
|
!include "x64.nsh"
|
||||||
|
!include "WinVer.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
!ifndef INFO_PROJECTNAME
|
||||||
|
!define INFO_PROJECTNAME "{{.Name}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COMPANYNAME
|
||||||
|
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTNAME
|
||||||
|
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_PRODUCTVERSION
|
||||||
|
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||||
|
!endif
|
||||||
|
!ifndef INFO_COPYRIGHT
|
||||||
|
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||||
|
!endif
|
||||||
|
!ifndef PRODUCT_EXECUTABLE
|
||||||
|
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||||
|
!endif
|
||||||
|
!ifndef UNINST_KEY_NAME
|
||||||
|
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||||
|
!endif
|
||||||
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||||
|
|
||||||
|
!ifndef REQUEST_EXECUTION_LEVEL
|
||||||
|
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_AMD64_BINARY
|
||||||
|
!define SUPPORTS_AMD64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef ARG_WAILS_ARM64_BINARY
|
||||||
|
!define SUPPORTS_ARM64
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "amd64_arm64"
|
||||||
|
!else
|
||||||
|
!define ARCH "amd64"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
!define ARCH "arm64"
|
||||||
|
!else
|
||||||
|
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||||
|
!endif
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!macro wails.checkArchitecture
|
||||||
|
!ifndef WAILS_WIN10_REQUIRED
|
||||||
|
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||||
|
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${If} ${AtLeastWin10}
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
IfSilent silentArch notSilentArch
|
||||||
|
silentArch:
|
||||||
|
SetErrorLevel 65
|
||||||
|
Abort
|
||||||
|
notSilentArch:
|
||||||
|
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||||
|
Quit
|
||||||
|
${else}
|
||||||
|
IfSilent silentWin notSilentWin
|
||||||
|
silentWin:
|
||||||
|
SetErrorLevel 64
|
||||||
|
Abort
|
||||||
|
notSilentWin:
|
||||||
|
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||||
|
Quit
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.files
|
||||||
|
!ifdef SUPPORTS_AMD64
|
||||||
|
${if} ${IsNativeAMD64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!ifdef SUPPORTS_ARM64
|
||||||
|
${if} ${IsNativeARM64}
|
||||||
|
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||||
|
${EndIf}
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.writeUninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
!ifdef WAILS_INSTALL_SCOPE
|
||||||
|
!if "${WAILS_INSTALL_SCOPE}" == "user"
|
||||||
|
WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKCU "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
!else
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
|
IntFmt $0 "0x%08X" $0
|
||||||
|
|
||||||
|
!ifdef WAILS_INSTALL_SCOPE
|
||||||
|
!if "${WAILS_INSTALL_SCOPE}" == "user"
|
||||||
|
WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!else
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.deleteUninstaller
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
|
||||||
|
!ifdef WAILS_INSTALL_SCOPE
|
||||||
|
!if "${WAILS_INSTALL_SCOPE}" == "user"
|
||||||
|
DeleteRegKey HKCU "${UNINST_KEY}"
|
||||||
|
!else
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
!endif
|
||||||
|
!else
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
!endif
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.setShellContext
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||||
|
SetShellVarContext all
|
||||||
|
${else}
|
||||||
|
SetShellVarContext current
|
||||||
|
${EndIf}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Install webview2 by launching the bootstrapper
|
||||||
|
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||||
|
!macro wails.webview2runtime
|
||||||
|
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||||
|
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
SetRegView 64
|
||||||
|
# If the admin key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||||
|
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||||
|
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||||
|
${If} $0 != ""
|
||||||
|
Goto ok
|
||||||
|
${EndIf}
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||||
|
SetDetailsPrint listonly
|
||||||
|
|
||||||
|
InitPluginsDir
|
||||||
|
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||||
|
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||||
|
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||||
|
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||||
|
|
||||||
|
SetDetailsPrint both
|
||||||
|
ok:
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||||
|
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||||
|
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||||
|
; Backup the previously associated file class
|
||||||
|
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||||
|
|
||||||
|
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateFiles
|
||||||
|
; Create file associations
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||||
|
|
||||||
|
File "..\{{.IconName}}.ico"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateFiles
|
||||||
|
; Delete app associations
|
||||||
|
{{range .Info.FileAssociations}}
|
||||||
|
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||||
|
|
||||||
|
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||||
|
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||||
|
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.associateCustomProtocols
|
||||||
|
; Create custom protocols associations
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro wails.unassociateCustomProtocols
|
||||||
|
; Delete app custom protocol associations
|
||||||
|
{{range .Info.Protocols}}
|
||||||
|
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||||
|
{{end}}
|
||||||
|
!macroend
|
||||||
15
build/windows/wails.exe.manifest
Normal file
15
build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
<asmv3:application>
|
||||||
|
<asmv3:windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||||
|
</asmv3:windowsSettings>
|
||||||
|
</asmv3:application>
|
||||||
|
</assembly>
|
||||||
27
config/plugin.json
Normal file
27
config/plugin.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"plugins": {
|
||||||
|
"iconfont": {
|
||||||
|
"enabled": false,
|
||||||
|
"description": "阿里IconFont图标库,支持在React中使用字体图标"
|
||||||
|
},
|
||||||
|
"ai": {
|
||||||
|
"enabled": false,
|
||||||
|
"description": "AI大模型集成,支持对话、分析等功能"
|
||||||
|
},
|
||||||
|
"concurrent": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": "高并发工具集,包含任务池、批量处理、限流器等"
|
||||||
|
},
|
||||||
|
"payment": {
|
||||||
|
"enabled": true,
|
||||||
|
"description": "支付授权与订单管理,支持登录、查授权、查账号、查订单、退款、账单",
|
||||||
|
"params": {
|
||||||
|
"app_id": "your_app_id",
|
||||||
|
"mch_id": "",
|
||||||
|
"api_key": "your_api_key",
|
||||||
|
"base_url": "https://api.example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>go_win_template</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script src="./src/main.tsx" type="module"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
1847
frontend/package-lock.json
generated
Normal file
1847
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
frontend/package.json
Normal file
22
frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.1.0",
|
||||||
|
"@types/react-dom": "^19.1.0",
|
||||||
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
"typescript": "^5.6.3",
|
||||||
|
"vite": "^7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/package.json.md5
Normal file
1
frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ebabc2ea396231dd6d5148053c60fb95
|
||||||
134
frontend/src/App.css
Normal file
134
frontend/src/App.css
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: #f0f2f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header p {
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
padding: 8px 20px;
|
||||||
|
background: #1677ff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #4096ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #52c41a;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
109
frontend/src/App.tsx
Normal file
109
frontend/src/App.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import './App.css';
|
||||||
|
import {
|
||||||
|
GetUserList,
|
||||||
|
CreateUser,
|
||||||
|
} from '../wailsjs/go/main/App';
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
const result = await GetUserList();
|
||||||
|
setUsers(result as User[]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
if (!name || !email) {
|
||||||
|
setMessage('请输入名称和邮箱');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await CreateUser(name, email);
|
||||||
|
if (result.code === 0) {
|
||||||
|
setMessage('创建成功');
|
||||||
|
setName('');
|
||||||
|
setEmail('');
|
||||||
|
await loadUsers();
|
||||||
|
} else {
|
||||||
|
setMessage(result.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<header className="header">
|
||||||
|
<h1>Wails + React + Go</h1>
|
||||||
|
<p>Windows 桌面应用框架示例</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="main">
|
||||||
|
<section className="card">
|
||||||
|
<h2>创建用户</h2>
|
||||||
|
<div className="form-row">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="名称"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
placeholder="邮箱"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button className="btn-primary" onClick={handleCreate}>
|
||||||
|
创建
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{message && <p className="message">{message}</p>}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h2>用户列表</h2>
|
||||||
|
<button className="btn-secondary" onClick={loadUsers}>
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{users.length === 0 ? (
|
||||||
|
<p className="empty">暂无数据</p>
|
||||||
|
) : (
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>邮箱</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{users.map((u) => (
|
||||||
|
<tr key={u.id}>
|
||||||
|
<td>{u.id}</td>
|
||||||
|
<td>{u.name}</td>
|
||||||
|
<td>{u.email}</td>
|
||||||
|
<td>{u.created_at}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
93
frontend/src/assets/fonts/OFL.txt
Normal file
93
frontend/src/assets/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
BIN
frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
Binary file not shown.
BIN
frontend/src/assets/images/logo-universal.png
Normal file
BIN
frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
14
frontend/src/main.tsx
Normal file
14
frontend/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {createRoot} from 'react-dom/client'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App'
|
||||||
|
|
||||||
|
const container = document.getElementById('root')
|
||||||
|
|
||||||
|
const root = createRoot(container!)
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App/>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
26
frontend/src/style.css
Normal file
26
frontend/src/style.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
html {
|
||||||
|
background-color: rgba(27, 38, 54, 1);
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
color: white;
|
||||||
|
font-family: "Nunito", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
||||||
|
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Nunito";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local(""),
|
||||||
|
url("assets/fonts/nunito-v16-latin-regular.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
31
frontend/tsconfig.json
Normal file
31
frontend/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
frontend/tsconfig.node.json
Normal file
11
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
13
frontend/vite.config.ts
Normal file
13
frontend/vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
// 端口被占用时自动尝试下一个,而不是直接报错退出
|
||||||
|
strictPort: false,
|
||||||
|
// 允许在同一台机器上同时跑多个 wails dev 实例时不冲突
|
||||||
|
cors: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
36
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
36
frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
import {main} from '../models';
|
||||||
|
import {context} from '../models';
|
||||||
|
|
||||||
|
export function BatchCreateUsers(arg1:Array<string>,arg2:Array<string>):Promise<main.Response>;
|
||||||
|
|
||||||
|
export function ClearUsers():Promise<number>;
|
||||||
|
|
||||||
|
export function CreateUser(arg1:string,arg2:string):Promise<main.Response>;
|
||||||
|
|
||||||
|
export function GetUserList():Promise<Array<main.User>>;
|
||||||
|
|
||||||
|
export function PayCheckAuth():Promise<main.Response>;
|
||||||
|
|
||||||
|
export function PayGetAccountInfo():Promise<main.Response>;
|
||||||
|
|
||||||
|
export function PayGetBill(arg1:number,arg2:number):Promise<main.Response>;
|
||||||
|
|
||||||
|
export function PayGetBillByDate(arg1:string,arg2:string):Promise<main.Response>;
|
||||||
|
|
||||||
|
export function PayLogin():Promise<main.Response>;
|
||||||
|
|
||||||
|
export function PayQueryOrder(arg1:string):Promise<main.Response>;
|
||||||
|
|
||||||
|
export function PayQueryOrderByOutNo(arg1:string):Promise<main.Response>;
|
||||||
|
|
||||||
|
export function PayRefreshToken():Promise<main.Response>;
|
||||||
|
|
||||||
|
export function PayRequestRefund(arg1:string,arg2:number,arg3:string):Promise<main.Response>;
|
||||||
|
|
||||||
|
export function Startup(arg1:context.Context):Promise<void>;
|
||||||
|
|
||||||
|
export function Stats():Promise<Record<string, any>>;
|
||||||
|
|
||||||
|
export function SubmitTask(arg1:string,arg2:string):Promise<main.Response>;
|
||||||
67
frontend/wailsjs/go/main/App.js
Normal file
67
frontend/wailsjs/go/main/App.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// @ts-check
|
||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function BatchCreateUsers(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['BatchCreateUsers'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClearUsers() {
|
||||||
|
return window['go']['main']['App']['ClearUsers']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CreateUser(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['CreateUser'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetUserList() {
|
||||||
|
return window['go']['main']['App']['GetUserList']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayCheckAuth() {
|
||||||
|
return window['go']['main']['App']['PayCheckAuth']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayGetAccountInfo() {
|
||||||
|
return window['go']['main']['App']['PayGetAccountInfo']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayGetBill(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['PayGetBill'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayGetBillByDate(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['PayGetBillByDate'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayLogin() {
|
||||||
|
return window['go']['main']['App']['PayLogin']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayQueryOrder(arg1) {
|
||||||
|
return window['go']['main']['App']['PayQueryOrder'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayQueryOrderByOutNo(arg1) {
|
||||||
|
return window['go']['main']['App']['PayQueryOrderByOutNo'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayRefreshToken() {
|
||||||
|
return window['go']['main']['App']['PayRefreshToken']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PayRequestRefund(arg1, arg2, arg3) {
|
||||||
|
return window['go']['main']['App']['PayRequestRefund'](arg1, arg2, arg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Startup(arg1) {
|
||||||
|
return window['go']['main']['App']['Startup'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stats() {
|
||||||
|
return window['go']['main']['App']['Stats']();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SubmitTask(arg1, arg2) {
|
||||||
|
return window['go']['main']['App']['SubmitTask'](arg1, arg2);
|
||||||
|
}
|
||||||
39
frontend/wailsjs/go/models.ts
Normal file
39
frontend/wailsjs/go/models.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export namespace main {
|
||||||
|
|
||||||
|
export class Response {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: any;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new Response(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.code = source["code"];
|
||||||
|
this.message = source["message"];
|
||||||
|
this.data = source["data"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class User {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
created_at: string;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new User(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.id = source["id"];
|
||||||
|
this.name = source["name"];
|
||||||
|
this.email = source["email"];
|
||||||
|
this.created_at = source["created_at"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
24
frontend/wailsjs/runtime/package.json
Normal file
24
frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@wailsapp/runtime",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Wails Javascript runtime library",
|
||||||
|
"main": "runtime.js",
|
||||||
|
"types": "runtime.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/wailsapp/wails.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Wails",
|
||||||
|
"Javascript",
|
||||||
|
"Go"
|
||||||
|
],
|
||||||
|
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/wailsapp/wails/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||||
|
}
|
||||||
330
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
330
frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Size {
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Screen {
|
||||||
|
isCurrent: boolean;
|
||||||
|
isPrimary: boolean;
|
||||||
|
width : number
|
||||||
|
height : number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment information such as platform, buildtype, ...
|
||||||
|
export interface EnvironmentInfo {
|
||||||
|
buildType: string;
|
||||||
|
platform: string;
|
||||||
|
arch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||||
|
// emits the given event. Optional data may be passed with the event.
|
||||||
|
// This will trigger any event listeners.
|
||||||
|
export function EventsEmit(eventName: string, ...data: any): void;
|
||||||
|
|
||||||
|
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||||
|
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||||
|
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||||
|
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||||
|
|
||||||
|
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||||
|
// sets up a listener for the given event name, but will only trigger once.
|
||||||
|
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||||
|
|
||||||
|
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||||
|
// unregisters the listener for the given event name.
|
||||||
|
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||||
|
|
||||||
|
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||||
|
// unregisters all listeners.
|
||||||
|
export function EventsOffAll(): void;
|
||||||
|
|
||||||
|
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||||
|
// logs the given message as a raw message
|
||||||
|
export function LogPrint(message: string): void;
|
||||||
|
|
||||||
|
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||||
|
// logs the given message at the `trace` log level.
|
||||||
|
export function LogTrace(message: string): void;
|
||||||
|
|
||||||
|
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||||
|
// logs the given message at the `debug` log level.
|
||||||
|
export function LogDebug(message: string): void;
|
||||||
|
|
||||||
|
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||||
|
// logs the given message at the `error` log level.
|
||||||
|
export function LogError(message: string): void;
|
||||||
|
|
||||||
|
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||||
|
// logs the given message at the `fatal` log level.
|
||||||
|
// The application will quit after calling this method.
|
||||||
|
export function LogFatal(message: string): void;
|
||||||
|
|
||||||
|
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||||
|
// logs the given message at the `info` log level.
|
||||||
|
export function LogInfo(message: string): void;
|
||||||
|
|
||||||
|
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||||
|
// logs the given message at the `warning` log level.
|
||||||
|
export function LogWarning(message: string): void;
|
||||||
|
|
||||||
|
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||||
|
// Forces a reload by the main application as well as connected browsers.
|
||||||
|
export function WindowReload(): void;
|
||||||
|
|
||||||
|
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||||
|
// Reloads the application frontend.
|
||||||
|
export function WindowReloadApp(): void;
|
||||||
|
|
||||||
|
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||||
|
// Sets the window AlwaysOnTop or not on top.
|
||||||
|
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||||
|
|
||||||
|
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window theme to system default (dark/light).
|
||||||
|
export function WindowSetSystemDefaultTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to light theme.
|
||||||
|
export function WindowSetLightTheme(): void;
|
||||||
|
|
||||||
|
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||||
|
// *Windows only*
|
||||||
|
// Sets window to dark theme.
|
||||||
|
export function WindowSetDarkTheme(): void;
|
||||||
|
|
||||||
|
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||||
|
// Centers the window on the monitor the window is currently on.
|
||||||
|
export function WindowCenter(): void;
|
||||||
|
|
||||||
|
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||||
|
// Sets the text in the window title bar.
|
||||||
|
export function WindowSetTitle(title: string): void;
|
||||||
|
|
||||||
|
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||||
|
// Makes the window full screen.
|
||||||
|
export function WindowFullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||||
|
// Restores the previous window dimensions and position prior to full screen.
|
||||||
|
export function WindowUnfullscreen(): void;
|
||||||
|
|
||||||
|
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||||
|
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||||
|
export function WindowIsFullscreen(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||||
|
// Sets the width and height of the window.
|
||||||
|
export function WindowSetSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||||
|
// Gets the width and height of the window.
|
||||||
|
export function WindowGetSize(): Promise<Size>;
|
||||||
|
|
||||||
|
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||||
|
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMaxSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||||
|
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||||
|
// Setting a size of 0,0 will disable this constraint.
|
||||||
|
export function WindowSetMinSize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||||
|
// Sets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowSetPosition(x: number, y: number): void;
|
||||||
|
|
||||||
|
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||||
|
// Gets the window position relative to the monitor the window is currently on.
|
||||||
|
export function WindowGetPosition(): Promise<Position>;
|
||||||
|
|
||||||
|
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||||
|
// Hides the window.
|
||||||
|
export function WindowHide(): void;
|
||||||
|
|
||||||
|
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||||
|
// Shows the window, if it is currently hidden.
|
||||||
|
export function WindowShow(): void;
|
||||||
|
|
||||||
|
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||||
|
// Maximises the window to fill the screen.
|
||||||
|
export function WindowMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||||
|
// Toggles between Maximised and UnMaximised.
|
||||||
|
export function WindowToggleMaximise(): void;
|
||||||
|
|
||||||
|
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||||
|
// Restores the window to the dimensions and position prior to maximising.
|
||||||
|
export function WindowUnmaximise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||||
|
export function WindowIsMaximised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||||
|
// Minimises the window.
|
||||||
|
export function WindowMinimise(): void;
|
||||||
|
|
||||||
|
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||||
|
// Restores the window to the dimensions and position prior to minimising.
|
||||||
|
export function WindowUnminimise(): void;
|
||||||
|
|
||||||
|
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||||
|
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||||
|
export function WindowIsMinimised(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||||
|
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||||
|
export function WindowIsNormal(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||||
|
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||||
|
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||||
|
|
||||||
|
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||||
|
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||||
|
export function ScreenGetAll(): Promise<Screen[]>;
|
||||||
|
|
||||||
|
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||||
|
// Opens the given URL in the system browser.
|
||||||
|
export function BrowserOpenURL(url: string): void;
|
||||||
|
|
||||||
|
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||||
|
// Returns information about the environment
|
||||||
|
export function Environment(): Promise<EnvironmentInfo>;
|
||||||
|
|
||||||
|
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||||
|
// Quits the application.
|
||||||
|
export function Quit(): void;
|
||||||
|
|
||||||
|
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||||
|
// Hides the application.
|
||||||
|
export function Hide(): void;
|
||||||
|
|
||||||
|
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||||
|
// Shows the application.
|
||||||
|
export function Show(): void;
|
||||||
|
|
||||||
|
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||||
|
// Returns the current text stored on clipboard
|
||||||
|
export function ClipboardGetText(): Promise<string>;
|
||||||
|
|
||||||
|
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||||
|
// Sets a text on the clipboard
|
||||||
|
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||||
|
|
||||||
|
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||||
|
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||||
|
|
||||||
|
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||||
|
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
export function OnFileDropOff() :void
|
||||||
|
|
||||||
|
// Check if the file path resolver is available
|
||||||
|
export function CanResolveFilePaths(): boolean;
|
||||||
|
|
||||||
|
// Resolves file paths for an array of files
|
||||||
|
export function ResolveFilePaths(files: File[]): void
|
||||||
|
|
||||||
|
// Notification types
|
||||||
|
export interface NotificationOptions {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string; // macOS and Linux only
|
||||||
|
body?: string;
|
||||||
|
categoryId?: string;
|
||||||
|
data?: { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationAction {
|
||||||
|
id?: string;
|
||||||
|
title?: string;
|
||||||
|
destructive?: boolean; // macOS-specific
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationCategory {
|
||||||
|
id?: string;
|
||||||
|
actions?: NotificationAction[];
|
||||||
|
hasReplyField?: boolean;
|
||||||
|
replyPlaceholder?: string;
|
||||||
|
replyButtonTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||||
|
// Initializes the notification service for the application.
|
||||||
|
// This must be called before sending any notifications.
|
||||||
|
export function InitializeNotifications(): Promise<void>;
|
||||||
|
|
||||||
|
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||||
|
// Cleans up notification resources and releases any held connections.
|
||||||
|
export function CleanupNotifications(): Promise<void>;
|
||||||
|
|
||||||
|
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||||
|
// Checks if notifications are available on the current platform.
|
||||||
|
export function IsNotificationAvailable(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||||
|
// Requests notification authorization from the user (macOS only).
|
||||||
|
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||||
|
// Checks the current notification authorization status (macOS only).
|
||||||
|
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||||
|
|
||||||
|
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||||
|
// Sends a basic notification with the given options.
|
||||||
|
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||||
|
|
||||||
|
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||||
|
// Sends a notification with action buttons. Requires a registered category.
|
||||||
|
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||||
|
|
||||||
|
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||||
|
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||||
|
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||||
|
// Removes a previously registered notification category.
|
||||||
|
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||||
|
// Removes all pending notifications from the notification center.
|
||||||
|
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||||
|
|
||||||
|
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||||
|
// Removes a specific pending notification by its identifier.
|
||||||
|
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||||
|
// Removes all delivered notifications from the notification center.
|
||||||
|
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||||
|
// Removes a specific delivered notification by its identifier.
|
||||||
|
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||||
|
|
||||||
|
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||||
|
// Removes a notification by its identifier (cross-platform convenience function).
|
||||||
|
export function RemoveNotification(identifier: string): Promise<void>;
|
||||||
298
frontend/wailsjs/runtime/runtime.js
Normal file
298
frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
_ __ _ __
|
||||||
|
| | / /___ _(_) /____
|
||||||
|
| | /| / / __ `/ / / ___/
|
||||||
|
| |/ |/ / /_/ / / (__ )
|
||||||
|
|__/|__/\__,_/_/_/____/
|
||||||
|
The electron alternative for Go
|
||||||
|
(c) Lea Anthony 2019-present
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function LogPrint(message) {
|
||||||
|
window.runtime.LogPrint(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogTrace(message) {
|
||||||
|
window.runtime.LogTrace(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogDebug(message) {
|
||||||
|
window.runtime.LogDebug(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogInfo(message) {
|
||||||
|
window.runtime.LogInfo(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogWarning(message) {
|
||||||
|
window.runtime.LogWarning(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogError(message) {
|
||||||
|
window.runtime.LogError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogFatal(message) {
|
||||||
|
window.runtime.LogFatal(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||||
|
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOn(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOff(eventName, ...additionalEventNames) {
|
||||||
|
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOffAll() {
|
||||||
|
return window.runtime.EventsOffAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsOnce(eventName, callback) {
|
||||||
|
return EventsOnMultiple(eventName, callback, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EventsEmit(eventName) {
|
||||||
|
let args = [eventName].slice.call(arguments);
|
||||||
|
return window.runtime.EventsEmit.apply(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReload() {
|
||||||
|
window.runtime.WindowReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowReloadApp() {
|
||||||
|
window.runtime.WindowReloadApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetAlwaysOnTop(b) {
|
||||||
|
window.runtime.WindowSetAlwaysOnTop(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSystemDefaultTheme() {
|
||||||
|
window.runtime.WindowSetSystemDefaultTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetLightTheme() {
|
||||||
|
window.runtime.WindowSetLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetDarkTheme() {
|
||||||
|
window.runtime.WindowSetDarkTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowCenter() {
|
||||||
|
window.runtime.WindowCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetTitle(title) {
|
||||||
|
window.runtime.WindowSetTitle(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowFullscreen() {
|
||||||
|
window.runtime.WindowFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnfullscreen() {
|
||||||
|
window.runtime.WindowUnfullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsFullscreen() {
|
||||||
|
return window.runtime.WindowIsFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetSize() {
|
||||||
|
return window.runtime.WindowGetSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetSize(width, height) {
|
||||||
|
window.runtime.WindowSetSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMaxSize(width, height) {
|
||||||
|
window.runtime.WindowSetMaxSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetMinSize(width, height) {
|
||||||
|
window.runtime.WindowSetMinSize(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetPosition(x, y) {
|
||||||
|
window.runtime.WindowSetPosition(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowGetPosition() {
|
||||||
|
return window.runtime.WindowGetPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowHide() {
|
||||||
|
window.runtime.WindowHide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowShow() {
|
||||||
|
window.runtime.WindowShow();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMaximise() {
|
||||||
|
window.runtime.WindowMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowToggleMaximise() {
|
||||||
|
window.runtime.WindowToggleMaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnmaximise() {
|
||||||
|
window.runtime.WindowUnmaximise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMaximised() {
|
||||||
|
return window.runtime.WindowIsMaximised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowMinimise() {
|
||||||
|
window.runtime.WindowMinimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowUnminimise() {
|
||||||
|
window.runtime.WindowUnminimise();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||||
|
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScreenGetAll() {
|
||||||
|
return window.runtime.ScreenGetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsMinimised() {
|
||||||
|
return window.runtime.WindowIsMinimised();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WindowIsNormal() {
|
||||||
|
return window.runtime.WindowIsNormal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BrowserOpenURL(url) {
|
||||||
|
window.runtime.BrowserOpenURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Environment() {
|
||||||
|
return window.runtime.Environment();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Quit() {
|
||||||
|
window.runtime.Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hide() {
|
||||||
|
window.runtime.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Show() {
|
||||||
|
window.runtime.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardGetText() {
|
||||||
|
return window.runtime.ClipboardGetText();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClipboardSetText(text) {
|
||||||
|
return window.runtime.ClipboardSetText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @callback OnFileDropCallback
|
||||||
|
* @param {number} x - x coordinate of the drop
|
||||||
|
* @param {number} y - y coordinate of the drop
|
||||||
|
* @param {string[]} paths - A list of file paths.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||||
|
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||||
|
*/
|
||||||
|
export function OnFileDrop(callback, useDropTarget) {
|
||||||
|
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||||
|
*/
|
||||||
|
export function OnFileDropOff() {
|
||||||
|
return window.runtime.OnFileDropOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CanResolveFilePaths() {
|
||||||
|
return window.runtime.CanResolveFilePaths();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResolveFilePaths(files) {
|
||||||
|
return window.runtime.ResolveFilePaths(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InitializeNotifications() {
|
||||||
|
return window.runtime.InitializeNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CleanupNotifications() {
|
||||||
|
return window.runtime.CleanupNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IsNotificationAvailable() {
|
||||||
|
return window.runtime.IsNotificationAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RequestNotificationAuthorization() {
|
||||||
|
return window.runtime.RequestNotificationAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckNotificationAuthorization() {
|
||||||
|
return window.runtime.CheckNotificationAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendNotification(options) {
|
||||||
|
return window.runtime.SendNotification(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SendNotificationWithActions(options) {
|
||||||
|
return window.runtime.SendNotificationWithActions(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterNotificationCategory(category) {
|
||||||
|
return window.runtime.RegisterNotificationCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveNotificationCategory(categoryId) {
|
||||||
|
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveAllPendingNotifications() {
|
||||||
|
return window.runtime.RemoveAllPendingNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemovePendingNotification(identifier) {
|
||||||
|
return window.runtime.RemovePendingNotification(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveAllDeliveredNotifications() {
|
||||||
|
return window.runtime.RemoveAllDeliveredNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveDeliveredNotification(identifier) {
|
||||||
|
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RemoveNotification(identifier) {
|
||||||
|
return window.runtime.RemoveNotification(identifier);
|
||||||
|
}
|
||||||
38
go.mod
Normal file
38
go.mod
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
module go_win_template
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require github.com/wailsapp/wails/v2 v2.16.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||||
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
|
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||||
|
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||||
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
|
golang.org/x/crypto v0.53.0 // indirect
|
||||||
|
golang.org/x/net v0.56.0 // indirect
|
||||||
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
|
golang.org/x/text v0.39.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
// replace github.com/wailsapp/wails/v2 v2.16.0 => C:\Users\12977\go\pkg\mod
|
||||||
83
go.sum
Normal file
83
go.sum
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||||
|
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||||
|
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||||
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||||
|
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||||
|
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||||
|
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||||
|
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||||
|
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||||
|
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||||
|
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||||
|
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||||
|
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||||
|
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
|
github.com/wailsapp/wails/v2 v2.16.0 h1:hLYrSHUUq6hRXl3rWT9ZXiyFIgM94nVN9Ns6iEJzxyw=
|
||||||
|
github.com/wailsapp/wails/v2 v2.16.0/go.mod h1:scxrgwfsv6yR6fE6cCF+Flfl+JeU+SR87T9x4kILJ6M=
|
||||||
|
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||||
|
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||||
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||||
|
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||||
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
|
||||||
|
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
48
internal/app/app.go
Normal file
48
internal/app/app.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Instance 全局应用单例,持有 ctx 和托盘引用
|
||||||
|
type Instance struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
ctx context.Context
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// App 应用上下文入口
|
||||||
|
type App struct {
|
||||||
|
instance *Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
var singleton *Instance
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
// Init 初始化应用全局上下文(只调用一次)
|
||||||
|
func Init() *App {
|
||||||
|
once.Do(func() {
|
||||||
|
singleton = &Instance{name: "go_win_template"}
|
||||||
|
})
|
||||||
|
return &App{instance: singleton}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSingleton 获取全局单例
|
||||||
|
func GetSingleton() *Instance {
|
||||||
|
return singleton
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContext 设置 Wails context
|
||||||
|
func (s *Instance) SetContext(ctx context.Context) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.ctx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context 获取当前 context
|
||||||
|
func (s *Instance) Context() context.Context {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
return s.ctx
|
||||||
|
}
|
||||||
62
internal/bus/bus.go
Normal file
62
internal/bus/bus.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package bus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler 事件处理器类型
|
||||||
|
type Handler func(payload any)
|
||||||
|
|
||||||
|
// EventBus 应用内事件总线,用于模块间解耦通信
|
||||||
|
type EventBus struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
handlers map[string][]Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventBus 创建事件总线实例
|
||||||
|
func NewEventBus() *EventBus {
|
||||||
|
return &EventBus{
|
||||||
|
handlers: make(map[string][]Handler),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe 订阅事件,返回取消函数
|
||||||
|
func (b *EventBus) Subscribe(event string, handler Handler) func() {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
b.handlers[event] = append(b.handlers[event], handler)
|
||||||
|
return func() { b.Unsubscribe(event, handler) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe 取消订阅
|
||||||
|
func (b *EventBus) Unsubscribe(event string, handler Handler) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
handlers := b.handlers[event]
|
||||||
|
for i, h := range handlers {
|
||||||
|
// 函数不能直接比较,用索引追踪
|
||||||
|
_ = i
|
||||||
|
_ = h
|
||||||
|
// 简化:直接清空该事件所有处理器(实际项目可用 handler ID)
|
||||||
|
b.handlers[event] = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish 同步发布事件
|
||||||
|
func (b *EventBus) Publish(event string, payload any) {
|
||||||
|
b.mu.RLock()
|
||||||
|
handlers := make([]Handler, len(b.handlers[event]))
|
||||||
|
copy(handlers, b.handlers[event])
|
||||||
|
b.mu.RUnlock()
|
||||||
|
for _, h := range handlers {
|
||||||
|
if h != nil {
|
||||||
|
h(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublishAsync 异步发布事件,不阻塞调用方
|
||||||
|
func (b *EventBus) PublishAsync(event string, payload any) {
|
||||||
|
b.Publish(event, payload) // 当前同步调用,后续可扩展为 goroutine
|
||||||
|
}
|
||||||
119
internal/cache/cache.go
vendored
Normal file
119
internal/cache/cache.go
vendored
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Item 缓存项
|
||||||
|
type Item struct {
|
||||||
|
Value any
|
||||||
|
Expiration int64 // Unix 纳秒,0 表示永不过期
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExpired 是否已过期
|
||||||
|
func (i *Item) IsExpired() bool {
|
||||||
|
if i.Expiration == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return time.Now().UnixNano() > i.Expiration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache 线程安全的内存缓存,支持 TTL
|
||||||
|
type Cache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
items map[string]*Item
|
||||||
|
ttl time.Duration
|
||||||
|
cleanup func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option 缓存配置选项
|
||||||
|
type Option func(*Cache)
|
||||||
|
|
||||||
|
// WithTTL 设置默认过期时间
|
||||||
|
func WithTTL(ttl time.Duration) Option {
|
||||||
|
return func(c *Cache) {
|
||||||
|
c.ttl = ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCleanupInterval 设置后台清理间隔
|
||||||
|
func WithCleanupInterval(d time.Duration) Option {
|
||||||
|
return func(c *Cache) {
|
||||||
|
go c.cleanupLoop(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCache 创建缓存实例
|
||||||
|
func NewCache(opts ...Option) *Cache {
|
||||||
|
c := &Cache{
|
||||||
|
items: make(map[string]*Item),
|
||||||
|
ttl: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 设置缓存项
|
||||||
|
func (c *Cache) Set(key string, value any) {
|
||||||
|
c.SetWithTTL(key, value, c.ttl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWithTTL 设置缓存项并指定过期时间
|
||||||
|
func (c *Cache) SetWithTTL(key string, value any, ttl time.Duration) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
exp := int64(0)
|
||||||
|
if ttl > 0 {
|
||||||
|
exp = time.Now().Add(ttl).UnixNano()
|
||||||
|
}
|
||||||
|
c.items[key] = &Item{Value: value, Expiration: exp}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取缓存项
|
||||||
|
func (c *Cache) Get(key string) (any, bool) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
item, ok := c.items[key]
|
||||||
|
if !ok || item.IsExpired() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return item.Value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除缓存项
|
||||||
|
func (c *Cache) Delete(key string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
delete(c.items, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len 返回缓存项数量
|
||||||
|
func (c *Cache) Len() int {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return len(c.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空缓存
|
||||||
|
func (c *Cache) Clear() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.items = make(map[string]*Item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) cleanupLoop(interval time.Duration) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
c.mu.Lock()
|
||||||
|
for k, v := range c.items {
|
||||||
|
if v.IsExpired() {
|
||||||
|
delete(c.items, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
272
internal/payment/client.go
Normal file
272
internal/payment/client.go
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// 配置与数据结构
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Config 支付平台配置,可接入任意支付网关
|
||||||
|
type Config struct {
|
||||||
|
AppID string `json:"app_id"`
|
||||||
|
MchID string `json:"mch_id"` // 商户号(可选)
|
||||||
|
APIKey string `json:"api_key"` // 签名密钥
|
||||||
|
BaseURL string `json:"base_url"` // 网关地址
|
||||||
|
Timeout time.Duration `json:"timeout"`
|
||||||
|
AutoRefreshToken bool `json:"auto_refresh_token"` // 是否自动续期
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token 授权令牌
|
||||||
|
type Token struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
IsExpired func() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Token) update(expiresIn int) {
|
||||||
|
t.ExpiresIn = expiresIn
|
||||||
|
t.ExpiresAt = time.Now().Add(time.Duration(expiresIn) * time.Second)
|
||||||
|
t.IsExpired = func() bool {
|
||||||
|
return time.Now().After(t.ExpiresAt.Add(-30 * time.Second)) // 提前30秒判定过期
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// HTTP 客户端(带自动重填和签名)
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Client 支付 HTTP 客户端
|
||||||
|
type Client struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
cfg Config
|
||||||
|
httpClient *http.Client
|
||||||
|
token *Token
|
||||||
|
tokenMu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建支付客户端
|
||||||
|
func NewClient(cfg Config) *Client {
|
||||||
|
if cfg.Timeout == 0 {
|
||||||
|
cfg.Timeout = 10 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.BaseURL == "" {
|
||||||
|
cfg.BaseURL = "https://api.example.com"
|
||||||
|
}
|
||||||
|
c := &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
httpClient: &http.Client{Timeout: cfg.Timeout},
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToken 获取访问令牌
|
||||||
|
func (c *Client) GetToken(ctx context.Context) (*Token, error) {
|
||||||
|
c.tokenMu.RLock()
|
||||||
|
if c.token != nil && !c.token.IsExpired() {
|
||||||
|
c.tokenMu.RUnlock()
|
||||||
|
return c.token, nil
|
||||||
|
}
|
||||||
|
c.tokenMu.RUnlock()
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
// double-check
|
||||||
|
c.tokenMu.RLock()
|
||||||
|
if c.token != nil && !c.token.IsExpired() {
|
||||||
|
c.tokenMu.RUnlock()
|
||||||
|
return c.token, nil
|
||||||
|
}
|
||||||
|
c.tokenMu.RUnlock()
|
||||||
|
|
||||||
|
reqURL := fmt.Sprintf("%s/oauth/token", c.cfg.BaseURL)
|
||||||
|
body := map[string]string{
|
||||||
|
"app_id": c.cfg.AppID,
|
||||||
|
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
||||||
|
"nonce": generateNonce(),
|
||||||
|
}
|
||||||
|
reqBody, _ := json.Marshal(body)
|
||||||
|
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(string(reqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
addSignature(req, body, c.cfg.APIKey)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get token: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
if result.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("get token failed: %s", result.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.token = &Token{}
|
||||||
|
c.token.update(result.Data.ExpiresIn)
|
||||||
|
c.token.AccessToken = result.Data.AccessToken
|
||||||
|
return c.token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken 主动刷新令牌
|
||||||
|
func (c *Client) RefreshToken(ctx context.Context) (*Token, error) {
|
||||||
|
reqURL := fmt.Sprintf("%s/oauth/refresh", c.cfg.BaseURL)
|
||||||
|
body := map[string]string{
|
||||||
|
"app_id": c.cfg.AppID,
|
||||||
|
"timestamp": fmt.Sprintf("%d", time.Now().Unix()),
|
||||||
|
}
|
||||||
|
reqBody, _ := json.Marshal(body)
|
||||||
|
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(string(reqBody)))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
addSignature(req, body, c.cfg.APIKey)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
|
||||||
|
if result.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("refresh token failed: %s", result.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
c.token = &Token{}
|
||||||
|
c.token.update(result.Data.ExpiresIn)
|
||||||
|
c.token.AccessToken = result.Data.AccessToken
|
||||||
|
c.mu.Unlock()
|
||||||
|
return c.token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 发送 GET 请求(自动携带 token)
|
||||||
|
func (c *Client) Get(ctx context.Context, path string, params map[string]string) ([]byte, error) {
|
||||||
|
token, err := c.GetToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.doRequest(ctx, http.MethodGet, path, params, token.AccessToken, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post 发送 POST 请求(自动携带 token)
|
||||||
|
func (c *Client) Post(ctx context.Context, path string, params map[string]string, body any) ([]byte, error) {
|
||||||
|
token, err := c.GetToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.doRequest(ctx, http.MethodPost, path, params, token.AccessToken, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doRequest(ctx context.Context, method, path string, params map[string]string, token string, body any) ([]byte, error) {
|
||||||
|
reqURL := c.cfg.BaseURL + path
|
||||||
|
values := url.Values{}
|
||||||
|
for k, v := range params {
|
||||||
|
values.Set(k, v)
|
||||||
|
}
|
||||||
|
if values.Encode() != "" {
|
||||||
|
reqURL += "?" + values.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqBody io.Reader
|
||||||
|
var contentType string
|
||||||
|
if body != nil {
|
||||||
|
data, _ := json.Marshal(body)
|
||||||
|
reqBody = strings.NewReader(string(data))
|
||||||
|
contentType = "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequestWithContext(ctx, method, reqURL, reqBody)
|
||||||
|
if contentType != "" {
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request %s %s: %w", method, path, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
return respBody, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// 签名辅助
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func addSignature(req *http.Request, body map[string]string, apiKey string) {
|
||||||
|
signStr := buildSignString(body)
|
||||||
|
sig := hmacSHA256(signStr, apiKey)
|
||||||
|
req.Header.Set("X-Signature", sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSignString(params map[string]string) string {
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for k := range params {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sortStrings(keys)
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, k := range keys {
|
||||||
|
sb.WriteString(k)
|
||||||
|
sb.WriteString("=")
|
||||||
|
sb.WriteString(params[k])
|
||||||
|
sb.WriteString("&")
|
||||||
|
}
|
||||||
|
return strings.TrimRight(sb.String(), "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hmacSHA256(data, key string) string {
|
||||||
|
m := hmac.New(sha256.New, []byte(key))
|
||||||
|
m.Write([]byte(data))
|
||||||
|
return fmt.Sprintf("%x", m.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortStrings(s []string) {
|
||||||
|
for i := 1; i < len(s); i++ {
|
||||||
|
for j := i; j > 0 && s[j] < s[j-1]; j-- {
|
||||||
|
s[j], s[j-1] = s[j-1], s[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateNonce() string {
|
||||||
|
// 简单 nonce,生产环境建议使用 crypto/rand
|
||||||
|
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
149
internal/payment/service.go
Normal file
149
internal/payment/service.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service 支付业务层,内部封装 Client,对外暴露方法
|
||||||
|
type Service struct {
|
||||||
|
client *Client
|
||||||
|
cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService 创建支付服务
|
||||||
|
func NewService(cfg Config) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: NewClient(cfg),
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClient 获取底层 HTTP 客户端(供高级用法)
|
||||||
|
func (s *Service) GetClient() *Client {
|
||||||
|
return s.client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 登录授权,获取 access_token
|
||||||
|
func (s *Service) Login(ctx context.Context) (map[string]any, error) {
|
||||||
|
token, err := s.client.GetToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"access_token": token.AccessToken,
|
||||||
|
"expires_in": token.ExpiresIn,
|
||||||
|
"message": "授权成功",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAuth 查询授权状态
|
||||||
|
func (s *Service) CheckAuth(ctx context.Context) (map[string]any, error) {
|
||||||
|
token, err := s.client.GetToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return map[string]any{"success": false, "message": "授权失效,请重新登录"}, err
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"access_token": token.AccessToken,
|
||||||
|
"expires_in": token.ExpiresIn,
|
||||||
|
"message": "授权有效",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh 主动刷新令牌
|
||||||
|
func (s *Service) Refresh(ctx context.Context) (map[string]any, error) {
|
||||||
|
token, err := s.client.RefreshToken(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"access_token": token.AccessToken,
|
||||||
|
"expires_in": token.ExpiresIn,
|
||||||
|
"message": "续期成功",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccountInfo 获取授权账号信息
|
||||||
|
func (s *Service) GetAccountInfo(ctx context.Context) (map[string]any, error) {
|
||||||
|
raw, err := s.client.Get(ctx, "/v1/account/info", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get account info: %w", err)
|
||||||
|
}
|
||||||
|
return parseAPIResponse(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryOrder 通过 order_id 查询订单
|
||||||
|
func (s *Service) QueryOrder(ctx context.Context, orderID string) (map[string]any, error) {
|
||||||
|
raw, err := s.client.Get(ctx, "/v1/orders/"+orderID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query order: %w", err)
|
||||||
|
}
|
||||||
|
return parseAPIResponse(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryOrderByOutNo 通过商户订单号查询
|
||||||
|
func (s *Service) QueryOrderByOutNo(ctx context.Context, outTradeNo string) (map[string]any, error) {
|
||||||
|
raw, err := s.client.Get(ctx, "/v1/orders", map[string]string{"out_trade_no": outTradeNo})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query order by out_no: %w", err)
|
||||||
|
}
|
||||||
|
return parseAPIResponse(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestRefund 申请退款
|
||||||
|
func (s *Service) RequestRefund(ctx context.Context, req map[string]any) (map[string]any, error) {
|
||||||
|
raw, err := s.client.Post(ctx, "/v1/refunds", nil, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request refund: %w", err)
|
||||||
|
}
|
||||||
|
return parseAPIResponse(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBill 分页获取账单
|
||||||
|
func (s *Service) GetBill(ctx context.Context, page, pageSize int) (map[string]any, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"page": fmt.Sprintf("%d", page),
|
||||||
|
"page_size": fmt.Sprintf("%d", pageSize),
|
||||||
|
}
|
||||||
|
raw, err := s.client.Get(ctx, "/v1/bills", params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get bill: %w", err)
|
||||||
|
}
|
||||||
|
return parseAPIResponse(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBillByDate 按日期范围查账单
|
||||||
|
func (s *Service) GetBillByDate(ctx context.Context, startDate, endDate string) (map[string]any, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"start_date": startDate,
|
||||||
|
"end_date": endDate,
|
||||||
|
}
|
||||||
|
raw, err := s.client.Get(ctx, "/v1/bills/range", params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get bill by date: %w", err)
|
||||||
|
}
|
||||||
|
return parseAPIResponse(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// 内部辅助
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
func parseAPIResponse(raw []byte) (map[string]any, error) {
|
||||||
|
var result struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.Code != 0 {
|
||||||
|
return nil, fmt.Errorf("api error code=%d msg=%s", result.Code, result.Message)
|
||||||
|
}
|
||||||
|
return map[string]any{"success": true, "data": result.Data, "message": result.Message}, nil
|
||||||
|
}
|
||||||
26
internal/plugins/ai_impl.go
Normal file
26
internal/plugins/ai_impl.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//go:build ai
|
||||||
|
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type aiPlugin struct {
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *aiPlugin) Name() string { return "ai" }
|
||||||
|
func (p *aiPlugin) Enabled() bool { return p.enabled }
|
||||||
|
func (p *aiPlugin) Init(ctx context.Context) error {
|
||||||
|
fmt.Println("[ai] initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (p *aiPlugin) Register(app *App) error { return nil }
|
||||||
|
func (p *aiPlugin) Shutdown() error { return nil }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg, _ := LoadOrDefault()
|
||||||
|
Register(&aiPlugin{enabled: cfg.Plugins["ai"].Enabled})
|
||||||
|
}
|
||||||
26
internal/plugins/concurrent_impl.go
Normal file
26
internal/plugins/concurrent_impl.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//go:build concurrent
|
||||||
|
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type concurrentPlugin struct {
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *concurrentPlugin) Name() string { return "concurrent" }
|
||||||
|
func (p *concurrentPlugin) Enabled() bool { return p.enabled }
|
||||||
|
func (p *concurrentPlugin) Init(ctx context.Context) error {
|
||||||
|
fmt.Println("[concurrent] initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (p *concurrentPlugin) Register(app *App) error { return nil }
|
||||||
|
func (p *concurrentPlugin) Shutdown() error { return nil }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg, _ := LoadOrDefault()
|
||||||
|
Register(&concurrentPlugin{enabled: cfg.Plugins["concurrent"].Enabled})
|
||||||
|
}
|
||||||
26
internal/plugins/iconfont_impl.go
Normal file
26
internal/plugins/iconfont_impl.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//go:build iconfont
|
||||||
|
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type iconfontPlugin struct {
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *iconfontPlugin) Name() string { return "iconfont" }
|
||||||
|
func (p *iconfontPlugin) Enabled() bool { return p.enabled }
|
||||||
|
func (p *iconfontPlugin) Init(ctx context.Context) error {
|
||||||
|
fmt.Println("[iconfont] initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (p *iconfontPlugin) Register(app *App) error { return nil }
|
||||||
|
func (p *iconfontPlugin) Shutdown() error { return nil }
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg, _ := LoadOrDefault()
|
||||||
|
Register(&iconfontPlugin{enabled: cfg.Plugins["iconfont"].Enabled})
|
||||||
|
}
|
||||||
153
internal/plugins/loader.go
Normal file
153
internal/plugins/loader.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 插件配置文件结构
|
||||||
|
type Config struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Plugins map[string]PluginOption `json:"plugins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PluginOption 单个插件配置项
|
||||||
|
type PluginOption struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
// Params 插件专用参数,由插件自己解析
|
||||||
|
Params map[string]interface{} `json:"params,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loader 插件配置加载器
|
||||||
|
type Loader struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
cfg *Config
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLoader 创建配置加载器
|
||||||
|
func NewLoader(path string) *Loader {
|
||||||
|
return &Loader{path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 从文件加载配置
|
||||||
|
func (l *Loader) Load() (*Config, error) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(l.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 环境变量覆盖优先级最高
|
||||||
|
if v := os.Getenv("PLUGIN_ICONFONT"); v == "false" || v == "0" {
|
||||||
|
tmp := cfg.Plugins["iconfont"]; tmp.Enabled = false; cfg.Plugins["iconfont"] = tmp
|
||||||
|
}
|
||||||
|
if v := os.Getenv("PLUGIN_AI"); v == "true" || v == "1" {
|
||||||
|
tmp := cfg.Plugins["ai"]; tmp.Enabled = true; cfg.Plugins["ai"] = tmp
|
||||||
|
}
|
||||||
|
if v := os.Getenv("PLUGIN_CONCURRENT"); v == "false" || v == "0" {
|
||||||
|
tmp := cfg.Plugins["concurrent"]; tmp.Enabled = false; cfg.Plugins["concurrent"] = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
l.cfg = &cfg
|
||||||
|
return l.cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled 查询插件启用状态
|
||||||
|
func (l *Loader) IsEnabled(name string) bool {
|
||||||
|
l.mu.RLock()
|
||||||
|
defer l.mu.RUnlock()
|
||||||
|
if l.cfg == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pc, ok := l.cfg.Plugins[name]
|
||||||
|
return ok && pc.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEnabledNames 返回所有已启用插件名
|
||||||
|
func (l *Loader) GetEnabledNames() []string {
|
||||||
|
l.mu.RLock()
|
||||||
|
defer l.mu.RUnlock()
|
||||||
|
if l.cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]string, 0)
|
||||||
|
for name, pc := range l.cfg.Plugins {
|
||||||
|
if pc.Enabled {
|
||||||
|
result = append(result, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 保存配置
|
||||||
|
func (l *Loader) Save(cfg *Config) error {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(l.path)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(l.path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadOrDefault 加载配置,找不到则返回默认值
|
||||||
|
func LoadOrDefault() (*Config, error) {
|
||||||
|
paths := []string{"config/plugin.json", "./plugin.json"}
|
||||||
|
exe, _ := os.Executable()
|
||||||
|
if exe != "" {
|
||||||
|
dir := filepath.Dir(exe)
|
||||||
|
paths = append([]string{
|
||||||
|
filepath.Join(dir, "config", "plugin.json"),
|
||||||
|
filepath.Join(dir, "plugin.json"),
|
||||||
|
}, paths...)
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
loader := NewLoader(p)
|
||||||
|
if cfg, err := loader.Load(); err == nil {
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureConfig 确保配置文件存在
|
||||||
|
func EnsureConfig() error {
|
||||||
|
_, err := os.Stat("config/plugin.json")
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cfg, _ := LoadOrDefault()
|
||||||
|
loader := NewLoader("config/plugin.json")
|
||||||
|
if mkErr := os.MkdirAll("config", 0755); mkErr != nil {
|
||||||
|
return mkErr
|
||||||
|
}
|
||||||
|
return loader.Save(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Version: "1.0.0",
|
||||||
|
Plugins: map[string]PluginOption{
|
||||||
|
"iconfont": {Enabled: false, Description: "阿里IconFont图标库,支持字体图标"},
|
||||||
|
"ai": {Enabled: false, Description: "AI大模型集成,支持对话、分析等功能"},
|
||||||
|
"concurrent": {Enabled: true, Description: "高并发工具集,任务池、限流器等"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
54
internal/plugins/payment_impl.go
Normal file
54
internal/plugins/payment_impl.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Package plugins — payment 插件实现(带 build tag,未启用时不编译)
|
||||||
|
//go:build payment
|
||||||
|
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go_win_template/internal/payment"
|
||||||
|
)
|
||||||
|
|
||||||
|
// paymentPluginImpl 支付插件
|
||||||
|
type paymentPluginImpl struct {
|
||||||
|
enabled bool
|
||||||
|
svc *payment.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *paymentPluginImpl) Name() string { return "payment" }
|
||||||
|
func (p *paymentPluginImpl) Enabled() bool { return p.enabled }
|
||||||
|
func (p *paymentPluginImpl) Init(ctx context.Context) error {
|
||||||
|
cfg, _ := LoadOrDefault()
|
||||||
|
pcfg := payment.Config{
|
||||||
|
AppID: getStringParam(cfg, "payment", "app_id"),
|
||||||
|
MchID: getStringParam(cfg, "payment", "mch_id"),
|
||||||
|
APIKey: getStringParam(cfg, "payment", "api_key"),
|
||||||
|
BaseURL: getStringParam(cfg, "payment", "base_url"),
|
||||||
|
AutoRefreshToken: true,
|
||||||
|
}
|
||||||
|
p.svc = payment.NewService(pcfg)
|
||||||
|
fmt.Printf("[payment] initialized, base_url=%s\n", pcfg.BaseURL)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (p *paymentPluginImpl) Register(app *App) error { return nil }
|
||||||
|
func (p *paymentPluginImpl) Shutdown() error { return nil }
|
||||||
|
|
||||||
|
// GetPaymentService 返回支付服务(any 类型,供 App 层类型断言)
|
||||||
|
func (p *paymentPluginImpl) GetPaymentService() any {
|
||||||
|
return p.svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg, _ := LoadOrDefault()
|
||||||
|
Register(&paymentPluginImpl{enabled: cfg.Plugins["payment"].Enabled})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringParam(cfg *Config, plugin, key string) string {
|
||||||
|
if opts, ok := cfg.Plugins[plugin]; ok {
|
||||||
|
if v, ok := opts.Params[key].(string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
150
internal/plugins/plugins.go
Normal file
150
internal/plugins/plugins.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
// Package plugins 插件系统核心接口与注册中心
|
||||||
|
package plugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugin 插件接口,所有插件必须实现
|
||||||
|
type Plugin interface {
|
||||||
|
Name() string
|
||||||
|
Init(ctx context.Context) error
|
||||||
|
Register(app *App) error
|
||||||
|
Shutdown() error
|
||||||
|
Enabled() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// App 插件可见的应用上下文
|
||||||
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
|
config map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApp 创建应用上下文
|
||||||
|
func NewApp(ctx context.Context, config map[string]interface{}) *App {
|
||||||
|
return &App{ctx: ctx, config: config}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry 插件注册中心(线程安全)
|
||||||
|
type Registry struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
plugins map[string]Plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalRegistry *Registry
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func getGlobalRegistry() *Registry {
|
||||||
|
once.Do(func() {
|
||||||
|
globalRegistry = &Registry{plugins: make(map[string]Plugin)}
|
||||||
|
})
|
||||||
|
return globalRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 全局注册插件(由各插件 build.go 的 init() 调用)
|
||||||
|
func Register(p Plugin) {
|
||||||
|
r := getGlobalRegistry()
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.plugins[p.Name()] = p
|
||||||
|
log.Printf("[plugins] registered: %s (enabled=%v)", p.Name(), p.Enabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取已注册插件
|
||||||
|
func Get(name string) Plugin {
|
||||||
|
r := getGlobalRegistry()
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return r.plugins[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 列出所有已注册插件名
|
||||||
|
func List() []string {
|
||||||
|
r := getGlobalRegistry()
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
names := make([]string, 0, len(r.plugins))
|
||||||
|
for name := range r.plugins {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count 返回已注册插件数
|
||||||
|
func Count() int {
|
||||||
|
r := getGlobalRegistry()
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return len(r.plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动所有已启用的插件
|
||||||
|
func Start(ctx context.Context) error {
|
||||||
|
r := getGlobalRegistry()
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
for name, p := range r.plugins {
|
||||||
|
if !p.Enabled() {
|
||||||
|
log.Printf("[plugins] skip disabled: %s", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := p.Init(ctx); err != nil {
|
||||||
|
log.Printf("[plugins] init %s failed: %v", name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := p.Register(NewApp(ctx, nil)); err != nil {
|
||||||
|
log.Printf("[plugins] register %s failed: %v", name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("[plugins] started: %s", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown 关闭所有插件
|
||||||
|
func Shutdown() error {
|
||||||
|
r := getGlobalRegistry()
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
for name, p := range r.plugins {
|
||||||
|
if err := p.Shutdown(); err != nil {
|
||||||
|
log.Printf("[plugins] shutdown %s error: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnabledPlugins 返回所有已启用插件列表
|
||||||
|
func EnabledPlugins() []Plugin {
|
||||||
|
r := getGlobalRegistry()
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
result := make([]Plugin, 0)
|
||||||
|
for _, p := range r.plugins {
|
||||||
|
if p.Enabled() {
|
||||||
|
result = append(result, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintStatus 打印当前插件状态(调试用)
|
||||||
|
func PrintStatus() {
|
||||||
|
r := getGlobalRegistry()
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
fmt.Println("[plugins] current status:")
|
||||||
|
for name, p := range r.plugins {
|
||||||
|
status := "disabled"
|
||||||
|
if p.Enabled() {
|
||||||
|
status = "enabled"
|
||||||
|
}
|
||||||
|
fmt.Printf(" - %s: %s\n", name, status)
|
||||||
|
}
|
||||||
|
if len(r.plugins) == 0 {
|
||||||
|
fmt.Println(" (no plugins registered — check build tags)")
|
||||||
|
}
|
||||||
|
}
|
||||||
369
internal/service/concurrent.go
Normal file
369
internal/service/concurrent.go
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BatchResult 批量任务结果
|
||||||
|
type BatchResult struct {
|
||||||
|
Success []any
|
||||||
|
Fail []BatchError
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchError 批量任务失败记录
|
||||||
|
type BatchError struct {
|
||||||
|
Index int
|
||||||
|
Item any
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchWorker 批量处理器,适合文件处理、数据转换等高并发场景
|
||||||
|
type BatchWorker struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
running atomic.Int64
|
||||||
|
total atomic.Int64
|
||||||
|
success atomic.Int64
|
||||||
|
failed atomic.Int64
|
||||||
|
maxConcurrency int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBatchWorker 创建批量处理器
|
||||||
|
func NewBatchWorker(maxConcurrency int) *BatchWorker {
|
||||||
|
if maxConcurrency <= 0 {
|
||||||
|
maxConcurrency = 10
|
||||||
|
}
|
||||||
|
return &BatchWorker{
|
||||||
|
maxConcurrency: maxConcurrency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessConcurrent 并发处理一批数据
|
||||||
|
func (bw *BatchWorker) ProcessConcurrent(
|
||||||
|
ctx context.Context,
|
||||||
|
items []any,
|
||||||
|
handler func(ctx context.Context, item any) (any, error),
|
||||||
|
) BatchResult {
|
||||||
|
bw.total.Add(int64(len(items)))
|
||||||
|
results := BatchResult{}
|
||||||
|
sem := make(chan struct{}, bw.maxConcurrency)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
for i, item := range items {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
sem <- struct{}{}
|
||||||
|
go func(index int, data any) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-sem }()
|
||||||
|
bw.running.Add(1)
|
||||||
|
defer bw.running.Add(-1)
|
||||||
|
|
||||||
|
result, err := handler(ctx, data)
|
||||||
|
bw.success.Add(1)
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
bw.failed.Add(1)
|
||||||
|
results.Fail = append(results.Fail, BatchError{
|
||||||
|
Index: index,
|
||||||
|
Item: data,
|
||||||
|
Err: err,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
results.Success = append(results.Success, result)
|
||||||
|
}
|
||||||
|
}(i, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessSequential 顺序处理(用于需要严格顺序的场景)
|
||||||
|
func (bw *BatchWorker) ProcessSequential(
|
||||||
|
ctx context.Context,
|
||||||
|
items []any,
|
||||||
|
handler func(ctx context.Context, item any) (any, error),
|
||||||
|
) BatchResult {
|
||||||
|
results := BatchResult{}
|
||||||
|
for i, item := range items {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return results
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
result, err := handler(ctx, item)
|
||||||
|
if err != nil {
|
||||||
|
results.Fail = append(results.Fail, BatchError{Index: i, Item: item, Err: err})
|
||||||
|
} else {
|
||||||
|
results.Success = append(results.Success, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats 当前处理统计
|
||||||
|
func (bw *BatchWorker) Stats() (running, total, success, failed int64) {
|
||||||
|
return bw.running.Load(), bw.total.Load(), bw.success.Load(), bw.failed.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RingBuffer 无锁环形缓冲区,适合高频消息传递
|
||||||
|
type RingBuffer struct {
|
||||||
|
data []any
|
||||||
|
head int64
|
||||||
|
tail int64
|
||||||
|
capacity int
|
||||||
|
size atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRingBuffer 创建环形缓冲区
|
||||||
|
func NewRingBuffer(capacity int) *RingBuffer {
|
||||||
|
if capacity <= 0 {
|
||||||
|
capacity = 1024
|
||||||
|
}
|
||||||
|
rb := &RingBuffer{
|
||||||
|
data: make([]any, capacity),
|
||||||
|
capacity: capacity,
|
||||||
|
}
|
||||||
|
return rb
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue 入队(阻塞直到有空位)
|
||||||
|
func (rb *RingBuffer) Enqueue(item any) {
|
||||||
|
for {
|
||||||
|
tail := atomic.LoadInt64(&rb.tail)
|
||||||
|
head := atomic.LoadInt64(&rb.head)
|
||||||
|
if tail-head >= int64(rb.capacity) {
|
||||||
|
time.Sleep(time.Microsecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if atomic.CompareAndSwapInt64(&rb.tail, tail, tail+1) {
|
||||||
|
rb.data[tail%int64(rb.capacity)] = item
|
||||||
|
rb.size.Add(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dequeue 出队(阻塞直到有数据)
|
||||||
|
func (rb *RingBuffer) Dequeue() (any, bool) {
|
||||||
|
for {
|
||||||
|
head := atomic.LoadInt64(&rb.head)
|
||||||
|
tail := atomic.LoadInt64(&rb.tail)
|
||||||
|
if head >= tail {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if atomic.CompareAndSwapInt64(&rb.head, head, head+1) {
|
||||||
|
item := rb.data[head%int64(rb.capacity)]
|
||||||
|
rb.data[head%int64(rb.capacity)] = nil
|
||||||
|
rb.size.Add(-1)
|
||||||
|
return item, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size 当前元素数量
|
||||||
|
func (rb *RingBuffer) Size() int64 {
|
||||||
|
return rb.size.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimiter 令牌桶限流器
|
||||||
|
type RateLimiter struct {
|
||||||
|
tokens int64
|
||||||
|
maxTokens int64
|
||||||
|
refillRate int64 // 每秒补充数
|
||||||
|
lastRefill time.Time
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRateLimiter 创建限流器
|
||||||
|
func NewRateLimiter(maxTokens, refillRate int64) *RateLimiter {
|
||||||
|
rl := &RateLimiter{
|
||||||
|
tokens: maxTokens,
|
||||||
|
maxTokens: maxTokens,
|
||||||
|
refillRate: refillRate,
|
||||||
|
lastRefill: time.Now(),
|
||||||
|
}
|
||||||
|
go rl.refillLoop()
|
||||||
|
return rl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) refillLoop() {
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
rl.mu.Lock()
|
||||||
|
elapsed := time.Since(rl.lastRefill).Seconds()
|
||||||
|
add := int64(elapsed * float64(rl.refillRate))
|
||||||
|
current := rl.tokens
|
||||||
|
if current+add > rl.maxTokens {
|
||||||
|
add = rl.maxTokens - current
|
||||||
|
}
|
||||||
|
rl.tokens += add
|
||||||
|
rl.lastRefill = time.Now()
|
||||||
|
rl.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire 获取一个令牌,返回是否成功
|
||||||
|
func (rl *RateLimiter) Acquire() bool {
|
||||||
|
rl.mu.Lock()
|
||||||
|
defer rl.mu.Unlock()
|
||||||
|
if rl.tokens <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
rl.tokens--
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit 带限制的包装函数
|
||||||
|
func (rl *RateLimiter) Limit(fn func() error) error {
|
||||||
|
for !rl.Acquire() {
|
||||||
|
time.Sleep(time.Millisecond * 10)
|
||||||
|
}
|
||||||
|
return fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConcurrentMap 线程安全的 map,支持并发读写
|
||||||
|
type ConcurrentMap struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
items map[any]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConcurrentMap 创建线程安全 map
|
||||||
|
func NewConcurrentMap() *ConcurrentMap {
|
||||||
|
return &ConcurrentMap{items: make(map[any]any)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConcurrentMap) Set(k, v any) {
|
||||||
|
cm.mu.Lock()
|
||||||
|
defer cm.mu.Unlock()
|
||||||
|
cm.items[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConcurrentMap) Get(k any) (any, bool) {
|
||||||
|
cm.mu.RLock()
|
||||||
|
defer cm.mu.RUnlock()
|
||||||
|
v, ok := cm.items[k]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConcurrentMap) Delete(k any) {
|
||||||
|
cm.mu.Lock()
|
||||||
|
defer cm.mu.Unlock()
|
||||||
|
delete(cm.items, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *ConcurrentMap) Len() int {
|
||||||
|
cm.mu.RLock()
|
||||||
|
defer cm.mu.RUnlock()
|
||||||
|
return len(cm.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipeline 函数式管道,链式处理数据流
|
||||||
|
type Pipeline struct {
|
||||||
|
stages []func(any) any
|
||||||
|
input chan any
|
||||||
|
output chan any
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPipeline 创建管道
|
||||||
|
func NewPipeline() *Pipeline {
|
||||||
|
return &Pipeline{
|
||||||
|
stages: make([]func(any) any, 0),
|
||||||
|
input: make(chan any, 256),
|
||||||
|
output: make(chan any, 256),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStage 添加处理阶段
|
||||||
|
func (p *Pipeline) AddStage(fn func(any) any) *Pipeline {
|
||||||
|
p.stages = append(p.stages, fn)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run 启动管道(阻塞,直到 input 关闭)
|
||||||
|
func (p *Pipeline) Run(items []any) []any {
|
||||||
|
go func() {
|
||||||
|
for _, item := range items {
|
||||||
|
p.input <- item
|
||||||
|
}
|
||||||
|
close(p.input)
|
||||||
|
}()
|
||||||
|
|
||||||
|
go p.process()
|
||||||
|
results := make([]any, 0)
|
||||||
|
for v := range p.output {
|
||||||
|
results = append(results, v)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pipeline) process() {
|
||||||
|
defer close(p.output)
|
||||||
|
for item := range p.input {
|
||||||
|
for _, stage := range p.stages {
|
||||||
|
item = stage(item)
|
||||||
|
}
|
||||||
|
p.output <- item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoCtx 便捷并发模式
|
||||||
|
type GoCtx struct {
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs sync.Map
|
||||||
|
results sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go 启动一个并发任务
|
||||||
|
func (gc *GoCtx) Go(id string, fn func() error) {
|
||||||
|
gc.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer gc.wg.Done()
|
||||||
|
if err := fn(); err != nil {
|
||||||
|
gc.errs.Store(id, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 等待所有任务完成,返回错误列表
|
||||||
|
func (gc *GoCtx) Wait() []error {
|
||||||
|
gc.wg.Wait()
|
||||||
|
var errs []error
|
||||||
|
gc.errs.Range(func(_, v any) bool {
|
||||||
|
errs = append(errs, v.(error))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitWithTimeout 带超时的等待
|
||||||
|
func (gc *GoCtx) WaitWithTimeout(timeout time.Duration) []error {
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
gc.wg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
var errs []error
|
||||||
|
gc.errs.Range(func(_, v any) bool {
|
||||||
|
errs = append(errs, v.(error))
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return errs
|
||||||
|
case <-time.After(timeout):
|
||||||
|
return []error{fmt.Errorf("并发任务超时: %v", timeout)}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
internal/tray/win/tray.go
Normal file
167
internal/tray/win/tray.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
// Package tray Windows 系统托盘实现(使用 win32 API)
|
||||||
|
package tray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hook 托盘图标句柄
|
||||||
|
var hook windows.Handle
|
||||||
|
|
||||||
|
// MenuHandle 菜单句柄
|
||||||
|
type MenuHandle uintptr
|
||||||
|
|
||||||
|
// Tray 系统托盘管理器
|
||||||
|
type Tray struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
visible bool
|
||||||
|
callback uintptr // WNDPROC 回调
|
||||||
|
hWnd windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// MenuItem 菜单项
|
||||||
|
type MenuItem struct {
|
||||||
|
ID uint32
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTray 创建系统托盘
|
||||||
|
func NewTray(iconData []byte, tooltip string) *Tray {
|
||||||
|
t := &Tray{}
|
||||||
|
if err := t.init(iconData, tooltip); err != nil {
|
||||||
|
log.Printf("[tray] init failed: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tray) init(iconData []byte, tooltip string) error {
|
||||||
|
// 获取当前进程窗口
|
||||||
|
hWnd, _ := windows.GetForegroundWindow()
|
||||||
|
if hWnd == 0 {
|
||||||
|
return windows.ERROR_ACCESS_DENIED
|
||||||
|
}
|
||||||
|
t.hWnd = hWnd
|
||||||
|
|
||||||
|
// 注册托盘图标
|
||||||
|
// NOTIFYICONDATA structure
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataa
|
||||||
|
data := ¬ifyIconData{
|
||||||
|
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||||
|
HWnd: uint32(hWnd),
|
||||||
|
UID: 1000,
|
||||||
|
UFlags: NIF_MESSAGE | NIF_ICON | NIF_TIP,
|
||||||
|
UCallbackMessage: WM_USER + 1000,
|
||||||
|
HIcon: 0, // 使用默认图标
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 tooltip
|
||||||
|
for i := 0; i < len(tooltip) && i < 128; i++ {
|
||||||
|
data.Tip[i] = uint8(tooltip[i])
|
||||||
|
}
|
||||||
|
data.Tip[len(tooltip)] = 0
|
||||||
|
|
||||||
|
// 添加托盘图标
|
||||||
|
ret, _, _ := procShellNotifyIcon.Call(
|
||||||
|
SHM_ADD,
|
||||||
|
uintptr(unsafe.Pointer(data)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return windows.GetLastError()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.visible = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy 销毁托盘图标
|
||||||
|
func (t *Tray) Destroy() {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
if !t.visible {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := ¬ifyIconData{
|
||||||
|
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||||
|
HWnd: uint32(t.hWnd),
|
||||||
|
UID: 1000,
|
||||||
|
UFlags: NIF_MESSAGE,
|
||||||
|
}
|
||||||
|
procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
|
||||||
|
t.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show 显示托盘
|
||||||
|
func (t *Tray) Show(iconData []byte, tooltip string) error {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
if t.visible {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t.init(iconData, tooltip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide 隐藏托盘
|
||||||
|
func (t *Tray) Hide() {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
if !t.visible {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data := ¬ifyIconData{
|
||||||
|
CbSize: uint32(unsafe.Sizeof(notifyIconData{})),
|
||||||
|
HWnd: uint32(t.hWnd),
|
||||||
|
UID: 1000,
|
||||||
|
UFlags: NIF_MESSAGE,
|
||||||
|
}
|
||||||
|
procShellNotifyIcon.Call(SHM_DELETE, uintptr(unsafe.Pointer(data)))
|
||||||
|
t.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsVisible 是否可见
|
||||||
|
func (t *Tray) IsVisible() bool {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
return t.visible
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// Win32 API 定义
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
const (
|
||||||
|
WM_USER = 0x0400
|
||||||
|
SHM_ADD uintptr = 0x00000000
|
||||||
|
SHM_DELETE uintptr = 0x00000001
|
||||||
|
|
||||||
|
NIF_MESSAGE = 0x00000001
|
||||||
|
NIF_ICON = 0x00000002
|
||||||
|
NIF_TIP = 0x00000004
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTIFYICONDATA struct
|
||||||
|
type notifyIconData struct {
|
||||||
|
CbSize uint32
|
||||||
|
HWnd uint32
|
||||||
|
UID uint32
|
||||||
|
UFlags uint32
|
||||||
|
UCallbackMessage uint32
|
||||||
|
HIcon uintptr
|
||||||
|
Tip [128]uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
procShellNotifyIcon = mustLoad("shell32.dll").MustFindProc("Shell_NotifyIconA")
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustLoad(dllName string) *windows.DynLib {
|
||||||
|
dll, err := windows.LoadDLL(dllName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("[tray] failed to load %s: %v", dllName, err)
|
||||||
|
}
|
||||||
|
return dll
|
||||||
|
}
|
||||||
146
internal/worker/pool.go
Normal file
146
internal/worker/pool.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Job 任务接口,所有后台任务需实现此接口
|
||||||
|
type Job interface {
|
||||||
|
Execute(ctx context.Context) error
|
||||||
|
JobID() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskResult 任务执行结果
|
||||||
|
type TaskResult struct {
|
||||||
|
JobID string
|
||||||
|
Output any
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool 任务并发池,管理固定数量工作协程
|
||||||
|
type Pool struct {
|
||||||
|
jobs chan Job
|
||||||
|
results chan TaskResult
|
||||||
|
wg sync.WaitGroup
|
||||||
|
stopOnce sync.Once
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
running atomic.Int64
|
||||||
|
total atomic.Int64
|
||||||
|
failed atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options 池配置
|
||||||
|
type Options struct {
|
||||||
|
Workers int // 工作协程数,默认 4
|
||||||
|
BufSize int // 队列缓冲,默认 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPool 创建任务池
|
||||||
|
func NewPool(opts ...Options) *Pool {
|
||||||
|
o := Options{Workers: 4, BufSize: 1000}
|
||||||
|
if len(opts) > 0 {
|
||||||
|
o = opts[0]
|
||||||
|
}
|
||||||
|
if o.Workers <= 0 {
|
||||||
|
o.Workers = 4
|
||||||
|
}
|
||||||
|
if o.BufSize <= 0 {
|
||||||
|
o.BufSize = 1000
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
p := &Pool{
|
||||||
|
jobs: make(chan Job, o.BufSize),
|
||||||
|
results: make(chan TaskResult, o.BufSize),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
p.startWorkers()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) startWorkers() {
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
p.wg.Add(1)
|
||||||
|
go p.worker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) worker() {
|
||||||
|
defer p.wg.Done()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return
|
||||||
|
case job, ok := <-p.jobs:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.running.Add(1)
|
||||||
|
p.total.Add(1)
|
||||||
|
err := job.Execute(p.ctx)
|
||||||
|
result := TaskResult{JobID: job.JobID(), Error: err}
|
||||||
|
if err != nil {
|
||||||
|
p.failed.Add(1)
|
||||||
|
}
|
||||||
|
p.running.Add(-1)
|
||||||
|
select {
|
||||||
|
case p.results <- result:
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit 提交任务到队列(非阻塞,队列满时返回 false)
|
||||||
|
func (p *Pool) Submit(job Job) bool {
|
||||||
|
select {
|
||||||
|
case p.jobs <- job:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitAndWait 提交任务并等待结果
|
||||||
|
func (p *Pool) SubmitAndWait(ctx context.Context, job Job) (*TaskResult, error) {
|
||||||
|
if !p.Submit(job) {
|
||||||
|
return nil, ErrQueueFull
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case r := <-p.results:
|
||||||
|
return &r, r.Error
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results 返回结果通道,供外部 range 消费
|
||||||
|
func (p *Pool) Results() <-chan TaskResult {
|
||||||
|
return p.results
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats 当前池状态
|
||||||
|
func (p *Pool) Stats() (running, total, failed int64) {
|
||||||
|
return p.running.Load(), p.total.Load(), p.failed.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 优雅停止池
|
||||||
|
func (p *Pool) Stop() {
|
||||||
|
p.stopOnce.Do(func() {
|
||||||
|
p.cancel()
|
||||||
|
close(p.jobs)
|
||||||
|
p.wg.Wait()
|
||||||
|
close(p.results)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrQueueFull = SyncError("任务队列已满,请降低提交频率或增大 BufSize")
|
||||||
|
|
||||||
|
// SyncError 用于类型断言
|
||||||
|
type SyncError string
|
||||||
|
|
||||||
|
func (e SyncError) Error() string { return string(e) }
|
||||||
50
main.go
Normal file
50
main.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v2"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
|
|
||||||
|
"go_win_template/internal/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed all:frontend/dist
|
||||||
|
//go:embed assets/icon.png
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
wailsApp := NewApp()
|
||||||
|
|
||||||
|
cfg := &options.App{
|
||||||
|
Title: "go_win_template",
|
||||||
|
Width: 1280,
|
||||||
|
Height: 800,
|
||||||
|
MinWidth: 1024,
|
||||||
|
MinHeight: 600,
|
||||||
|
WindowStartState: options.Normal,
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||||
|
OnStartup: func(ctx context.Context) {
|
||||||
|
log.Println("[main] startup complete")
|
||||||
|
// 启动插件
|
||||||
|
if err := plugins.Start(ctx); err != nil {
|
||||||
|
log.Printf("[main] plugin start error: %v", err)
|
||||||
|
}
|
||||||
|
plugins.PrintStatus()
|
||||||
|
},
|
||||||
|
Bind: []interface{}{
|
||||||
|
wailsApp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := wails.Run(cfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
trayicons/icon.png
Normal file
BIN
trayicons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 477 B |
18
wails.json
Normal file
18
wails.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||||
|
"name": "go_win_template",
|
||||||
|
"outputfilename": "go_win_template",
|
||||||
|
"frontend:install": "npm install",
|
||||||
|
"frontend:build": "npm run build",
|
||||||
|
"frontend:dev:watcher": "npm run dev",
|
||||||
|
"frontend:dev:serverUrl": "auto",
|
||||||
|
"author": {
|
||||||
|
"name": "Geliebte",
|
||||||
|
"email": "1297754537@qq.com"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"productname": "go_win_template",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"copyright": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user