Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fd8d46745 | |||
| c5a72ecbdf | |||
| c553237f15 | |||
| 02082df525 |
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.36] - 2026-06-13
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复数据库初始化时 license_key 索引超长导致启动失败的问题
|
||||||
|
|
||||||
|
## [1.0.35] - 2026-06-12
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
- Debug 版本窗口标题增加 [DEBUG] 后缀,便于与正式安装版区分
|
||||||
|
|
||||||
## [1.0.34] - 2026-06-12
|
## [1.0.34] - 2026-06-12
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "time"
|
|||||||
type License struct {
|
type License struct {
|
||||||
Base
|
Base
|
||||||
ShopID uint64 `gorm:"not null;index" json:"shop_id"`
|
ShopID uint64 `gorm:"not null;index" json:"shop_id"`
|
||||||
LicenseKey string `gorm:"size:2048;uniqueIndex" json:"license_key"`
|
LicenseKey string `gorm:"size:768;uniqueIndex" json:"license_key"`
|
||||||
Type string `gorm:"type:enum('trial','monthly','annual','lifetime');default:'trial'" json:"type"`
|
Type string `gorm:"type:enum('trial','monthly','annual','lifetime');default:'trial'" json:"type"`
|
||||||
ExpiresAt *time.Time `json:"expires_at"`
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
|
|||||||
@@ -73,3 +73,38 @@ func TestVerifyLicenseToken_InvalidFormat(t *testing.T) {
|
|||||||
_, err = VerifyLicenseToken("not-a-valid-token", pub)
|
_, err = VerifyLicenseToken("not-a-valid-token", pub)
|
||||||
assert.ErrorIs(t, err, ErrInvalidLicenseToken)
|
assert.ErrorIs(t, err, ErrInvalidLicenseToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLicenseTokenFitsColumnLimit verifies that a realistic (even worst-case)
|
||||||
|
// license token fits within the VARCHAR(768) column limit imposed by the
|
||||||
|
// InnoDB index constraint (768 chars × 4 bytes/char = 3072 bytes max).
|
||||||
|
func TestLicenseTokenFitsColumnLimit(t *testing.T) {
|
||||||
|
const columnLimit = 768
|
||||||
|
|
||||||
|
priv, _, err := GenerateEd25519KeyPair()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Use a large features map to simulate a worst-case payload.
|
||||||
|
exp := time.Now().Add(365 * 24 * time.Hour).Unix()
|
||||||
|
payload := LicensePayload{
|
||||||
|
ShopID: 999999999,
|
||||||
|
LicenseID: 999999999,
|
||||||
|
Type: "lifetime",
|
||||||
|
IssuedAt: time.Now().Unix(),
|
||||||
|
ExpiresAt: &exp,
|
||||||
|
MaxDevices: 99,
|
||||||
|
Features: map[string]any{
|
||||||
|
"finance": true,
|
||||||
|
"inventory": true,
|
||||||
|
"reports": true,
|
||||||
|
"export": true,
|
||||||
|
"multi_shop": true,
|
||||||
|
"api_access": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := IssueLicenseToken(payload, priv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.LessOrEqual(t, len(token), columnLimit,
|
||||||
|
"license token length %d exceeds VARCHAR(%d) column limit", len(token), columnLimit)
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ CREATE TABLE IF NOT EXISTS `users` (
|
|||||||
CREATE TABLE IF NOT EXISTS `licenses` (
|
CREATE TABLE IF NOT EXISTS `licenses` (
|
||||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`shop_id` BIGINT UNSIGNED NOT NULL,
|
`shop_id` BIGINT UNSIGNED NOT NULL,
|
||||||
`license_key` VARCHAR(2048) NOT NULL COMMENT 'Ed25519 signed token',
|
`license_key` VARCHAR(768) NOT NULL COMMENT 'Ed25519 signed token',
|
||||||
`device_id` VARCHAR(255) DEFAULT NULL COMMENT 'deprecated: use license_devices',
|
`device_id` VARCHAR(255) DEFAULT NULL COMMENT 'deprecated: use license_devices',
|
||||||
`type` ENUM('trial','monthly','annual','lifetime') NOT NULL DEFAULT 'trial',
|
`type` ENUM('trial','monthly','annual','lifetime') NOT NULL DEFAULT 'trial',
|
||||||
`expires_at` DATETIME DEFAULT NULL COMMENT 'NULL=永久',
|
`expires_at` DATETIME DEFAULT NULL COMMENT 'NULL=永久',
|
||||||
@@ -69,7 +69,7 @@ CREATE TABLE IF NOT EXISTS `licenses` (
|
|||||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_license_key` (`license_key`(255)),
|
UNIQUE KEY `uk_license_key` (`license_key`),
|
||||||
KEY `idx_shop_id` (`shop_id`)
|
KEY `idx_shop_id` (`shop_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许可证';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='许可证';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart' show kDebugMode;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
@@ -107,7 +108,7 @@ class _JiuAppState extends ConsumerState<JiuApp> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final router = ref.watch(appRouterProvider);
|
final router = ref.watch(appRouterProvider);
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: '岩美',
|
title: kDebugMode ? '岩美 [DEBUG]' : '岩美',
|
||||||
theme: AppTheme.light(),
|
theme: AppTheme.light(),
|
||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user