activity-code-standards
Vibe Coding Style Guide
go语言版本: go 1.24.0
1. 项目结构 (Project Structure)
1.1 标准目录布局
app/activity/{year}/{activity_name}/
├── internal/
│ ├── conf/ # 配置与常量
│ ├── store/ # Redis 数据层
│ ├── service/ # 业务逻辑层
│ ├── event/ # 事件处理
│ ├── cron/ # 定时任务
│ └── acttrack/ # 埋点上报(可选)
└── router/ # 路由注册
2. 命名规范 (Naming Conventions)
2.1 常量命名
// ✅ 使用大驼峰,按功能分组
const (
ActID = 5811
ChoseRewardTaskID = 5
ShareTaskNoRepeatTaskID = 13
RankTypeGrowth = "growth"
DeferTaskGrowth = "growth_track"
)
2.2 配置结构体命名
// ✅ 主配置:SpecialConfig
type SpecialConfig struct {
TargetVipLevel int `json:"target_vip_level"`
PartyConfig *PartyConfig `json:"party_config"`
Msg Message `json:"msg"`
}
// ✅ 子配置:按功能后缀 Config
type PartyConfig struct {
RedPacketNpcID int `json:"red_packet_npc_id"`
StayRoomDuration int `json:"stay_room_duration"`
}
// ✅ 消息配置:Message
type Message struct {
LoginMsg weactivity.Message `json:"login_msg"`
PartyStartMsg weactivity.Message `json:"party_start_msg"`
}
2.3 函数命名
// ✅ 事件处理:Handle + 事件名
func HandleLoginApp(data string) {}
func HandleSendGift(data string) {}
func HandleVipUpgrade(data string) {}
// ✅ 业务接口:动词 + 名词
func GetInfo(ctx context.Context, req *actutil.CommonReq) (*store.GetInfoRsp, error) {}
func ChoseRewardApi(ctx context.Context, req *store.ChoseRewardReq) (*store.ChoseRewardRsp, error) {}
// ✅ 工具函数:Can/Check/Get 开头
func CanUserJoinAct(config *conf.SpecialConfig, uid int) bool {}
func CheckCanRecvReward(actInfo *weactivity.Activity) bool {}
func GetUserFirstVipLevel(uid int) (int, error) {}
2.4 Redis Key 命名
// ✅ 变量名:大驼峰 + 语义化
var UserCanJoinAct = actredis.NewClient(...)
var PartyGrowthInfo = actredis.NewClient(...)
// ✅ KeyBase:活动简称:功能:参数占位符
KeyBase: "ar_super_bigr:party_growth_info:%d"
KeyBase: "ar_bigr_normal:user_send_gift_info:%d:%d"
// ✅ Field 常量:统一前缀 Field + 大驼峰
const (
FieldPartyGrowthPercent = "party_growth_percent"
FieldPartyGrowthEndTime = "party_growth_end_time"
)
// ✅ 提供 Getter 避免硬编码
func FieldCombo() string { return fieldCombo }
func FieldTotal() string { return fieldTotal }
3. 代码组织 (Code Organization)
3.1 函数顺序
// ✅ 推荐顺序(从公开到私有)
package service
// 1. 对外接口(按字母序)
func ChoseRewardApi(ctx context.Context, req *store.ChoseRewardReq) (*store.ChoseRewardRsp, error) {}
func GetInfo(ctx context.Context, req *actutil.CommonReq) (*store.GetInfoRsp, error) {}
func RecvTask(ctx context.Context, req *store.RecvTaskReq) (*store.RecvTaskRsp, error) {}
// 2. 内部辅助函数
func canUserJoinAct(config *conf.SpecialConfig, uid int) bool {}
func validateRewardRequest(req *store.ChoseRewardReq) error {}
4. 错误处理 (Error Handling)
4.1 参数校验
// ✅ 早返回 (Early Return)
if eventData.Uid <= 0 || eventData.Coin <= 0 {
return
}
if req.VrCenterID <= 0 {
return nil, acterror.NewCustomError(actmsg.ParamsErr)
}
4.2 依赖检查
// ✅ 逐层校验配置
actInfo := actinfo.GetActById(conf.ActID)
if actInfo == nil {
return rsp, acterror.NewCustomError(actmsg.ActTimeErr)
}
specialConfig := conf.GetSpecialConfig(actInfo)
if specialConfig == nil {
return rsp, acterror.NewCustomError(actmsg.ConfigErr)
}
4.3 错误日志
// ✅ 错误时记录关键上下文(例如ChoseReward方法)
if err != nil {
entry.WithError(err).Errorln("ar_super_bigr chose reward SendRewards err")
return rsp, err
}
5. 并发控制 (Concurrency)
5.1 分布式锁
使用 actutil.TryLockUidWithRetry / TryLockUidOnce / TryLockWithRetryWithSuffix 等封装函数(来自 app/activity/common/actutil/redis_lock.go)。
禁止直接使用 actstore.SetExNx / actstore.SetNx 实现分布式锁。
// ✅ 细粒度锁 + 自动重试(有业务后缀维度时)
lock := actutil.TryLockWithRetryWithSuffix(
conf.ActID, // 活动 ID
req.StageID, // 业务维度 1(int)
strconv.Itoa(req.Index), // 业务维度 2(string 后缀)
10, // 超时秒数
)
if lock.Failed() {
return rsp, acterror.NewCustomError(actmsg.LockErr)
}
defer lock.Unlock()
// ✅ 仅需 uid 级别锁时
lock := actutil.TryLockUidWithRetry(conf.ActID, uid, 10)
if lock.Failed() {
return rsp, acterror.NewCustomError(actmsg.LockErr)
}
defer lock.Unlock()
// ❌ 错误:直接用 actstore.SetExNx 实现锁
ok, _ := actstore.SetExNx(lockKey, 1, 10)
if !ok {
return rsp, acterror.NewCustomError(actmsg.LockErr)
}
// 缺少 Unlock、缺少重试、缺少标准化 Key 格式
5.3 异步非阻塞
// ✅ 非关键路径使用 safego
safego.SafeGo(func() {
store.GrowthEnjoyUserNumber.BuildKey(ctx, rid, growthId).SAdd(sendUid)
store.GrowthEnjoyCount.BuildKey(ctx, rid, growthId).HIncrBy(store.FieldGrowthAddScore, needAddVal)
})
6. Redis 使用 (Redis Best Practices)
6.1 Key 定义标准
// ✅ 使用 actredis 统一封装
var UserSendGiftInfo = actredis.NewClient(db.ActiveRedis, actredis.Extra{
KeyBase: "ar_bigr_normal:user_send_gift_info:%d:%d", // 活动简称:功能:参数
ActId: conf.ActID, // 自动添加前缀
KeyWithActId: true, // 是否包含 ActID
Desc: "用户送礼信息(uid, gift_id)", // 中文描述
}).GetHashRedis()
6.2 原子操作
// ✅ 使用 IncrBy + 判断阈值
afterSeconds, err := store.StayRoomSeconds.BuildKey(ctx, rid).HIncrBy(uid, duration).Result()
beforeSeconds := afterSeconds - duration
if afterSeconds >= threshold && beforeSeconds < threshold {
// 触发奖励
}
7. 日志规范 (Logging Standards)
7.1 日志初始化
// ✅ 使用 actlogger 带活动上下文
ctx := actlogger.GenCtx(conf.ActID, uid)
entry := actlogger.GetLogger(ctx).WithFields(logrus.Fields{
"func": "HandleSendGift",
"event": event,
})
7.2 日志级别
// ✅ Info:关键流程节点
entry.Infoln("ar_super_bigr HandleLoginApp send party login msg success")
// ✅ Warning:预期内的异常(如库存不足)
entry.WithError(err).Warningln("ar super bigr chose reward ReceiveTaskReward err")
// ✅ Error:需要人工介入的错误
entry.WithError(err).Errorln("ar_super_bigr HandleSendGift HIncrBy total err")
// ✅ Debug:调试信息
entry.Debugln("ar_super_bigr HandleVoiceRoom rid is not in start party rid set")
7.3 日志前缀
// ✅ 统一格式:活动简称 + 函数名/模块名 + 动作
"ar_super_bigr HandlePartyVal target vr center start"
"ar_super_bigr GetPartyInfo GetRankList err"
8. 配置解析 (Configuration Parsing)
8.1 GetSpecialConfig 实现规范
GetSpecialConfig 内部必须使用 actinfo.ParseSpecialInfoV2 进行配置解析,禁止直接 json.Unmarshal。
// ✅ GetSpecialConfig 标准实现
func GetSpecialConfig(actInfo *weactivity.Activity) *SpecialConfig {
if actInfo == nil {
return nil
}
specialConfig := &SpecialConfig{}
if err := actinfo.ParseSpecialInfoV2(actInfo, specialConfig); err != nil {
return nil
}
return specialConfig
}
// ❌ 错误:直接 json.Unmarshal
func GetSpecialConfig(actInfo *weactivity.Activity) *SpecialConfig {
specialConfig := &SpecialConfig{}
if err := json.Unmarshal(actInfo.SpecialInfo, specialConfig); err != nil {
return nil
}
return specialConfig
}
8.2 业务逻辑前校验
// ✅ 业务逻辑前先校验配置完整性
actInfo := actinfo.GetActById(conf.ActID)
specialConfig := conf.GetSpecialConfig(actInfo)
if specialConfig == nil {
return
}
8.3 SpecialConfig 设计原则
✅ 奖励和概率放入 SpecialConfig,不写成常量
奖励 ID、概率权重、数值倍率、时长等可配置的业务数值,必须通过 SpecialConfig 字段下发,禁止硬编码为 Go 常量。
奖励配置:用 []*weactivity.Reward 或自定义结构体内嵌 []*weactivity.Reward 表达。
// ✅ 正确:奖励放 SpecialConfig
type SpecialConfig struct {
// 扭蛋机配置(含奖励ID和权重,通过后台配置)
GachaMachines []GachaMachineConfig `json:"gacha_machines"`
// 日榜 buff 倍率(key: "1"/"2"/"3" → 倍率)
DailyRoomBuffRates map[string]float64 `json:"daily_room_buff_rates"`
// 语音房 S 级加成倍率
VoiceRoomSBuffRate float64 `json:"voice_room_s_buff_rate"`
}
type GachaMachineConfig struct {
MachineType string `json:"machine_type"`
GuaranteeCount int `json:"guarantee_count"`
Rewards []*weactivity.Reward `json:"rewards"` // ✅ 用 weactivity.Reward 承载奖励
}
// ❌ 错误:奖励ID和数值倍率写成常量
const (
DailyRoomBuffTop1 = 2.0
DefaultVoiceRoomSBuff = 2.0
GachaIntimacyGift1 = 1722191
)
var DefaultGachaMachines = []GachaMachineConfig{...} // ❌ 不应有写死奖励ID的默认变量
发放奖励时配合 actreward.ParseRewards 转换(见 12.3):
rewardInfos := actreward.ParseRewards(uid, machineConf.Rewards)
actreward.SendRewards(uid, rewardInfos, &actreward.ExtraInfo{ActId: conf.ActID})
8.4 Hook 注册规范
钩子必须通过 GetHookers() 返回 actmodel.ActHookerList 的方式注册,禁止在 init() 中调用全局 RegisterXxxHooker。
在实现前,必须阅读 app/activity/common/actmodel/actmodel.go 中的 ActHookerList 结构体,找到对应 Hook 类型的字段名,再在 GetHookers() 中填充。
// ✅ 正确:通过 GetHookers() 注册
func (a *Model) GetHookers() actmodel.ActHookerList {
return actmodel.ActHookerList{
LotteryHooker: &hook.SakuraLotteryHooker{},
TaskHooker: &hook.SakuraTaskHooker{},
RankHooker: &hook.SakuraRankHooker{},
BlindBoxHooks: map[int]gift.BlindBoxHook{
conf.GiftIDBlindBox: &hook.SakuraBlindBoxHook{},
},
GiftBoxHooks: map[int]gift.GiftBoxHook{
conf.GiftIDGiftBox: &hook.SakuraGiftBoxHook{},
},
}
}
// ❌ 错误:在 init() 或 hook 文件内全局注册
func init() {
lottery.RegisterLotteryHooker(conf.ActID, &SakuraLotteryHooker{})
gift.RegisterBlindBoxHook(conf.GiftIDBlindBox, &SakuraBlindBoxHook{})
}
确认字段名的步骤:
- 打开
app/activity/common/actmodel/actmodel.go - 在
ActHookerListstruct 中找到对应类型的字段(如LotteryHooker lottery.LotteryHooker、BlindBoxHooks map[int]gift.BlindBoxHook) - 在
GetHookers()中按字段名填入实例
9. 事件处理 (Event Handling)
9.1 事件函数结构
func HandleEventName(data string) {
// 1. 反序列化
eventData := &actevent.EventType{}
if err := json.Unmarshal([]byte(data), eventData); err != nil {
logrus.WithError(err).Errorln("activity_name HandleEventName Unmarshal err")
return
}
// 2. 参数校验
if eventData.Uid <= 0 {
return
}
// 3. 活动校验
actInfo := actinfo.GetValidActByUid(conf.ActID, eventData.Uid)
if actInfo == nil {
return
}
// 4. 配置获取
specialInfo := conf.GetSpecialConfig(actInfo)
if specialInfo == nil {
return
}
// 5. 业务逻辑
ctx := actlogger.GenCtx(conf.ActID, eventData.Uid)
// ...
}
9.2 事件过滤
// ✅ 多条件过滤前置
if eventData.ActId != conf.ActID {
return
}
if eventData.ChipId != conf.GrowthChipID {
return
}
if !helper.InArrayInt(event.GiftId, specialInfo.TargetGiftIDList) {
return
}
10. 接口设计 (API Design)
10.1 请求响应结构
// ✅ Request:统一后缀 Req
type ChoseRewardReq struct {
ActId int `json:"act_id"`
Uid int `json:"uid"`
StageID int `json:"stage_id"`
Index int `json:"index"`
}
// ✅ Response:统一后缀 Rsp(非 Resp)
type ChoseRewardRsp struct {
RewardInfo *actreward.RewardInfo `json:"reward_info"`
}
10.2 接口函数签名
// ✅ 标准签名:(ctx, req) (rsp, error)
func GetInfo(c context.Context, req *actutil.CommonReq) (*store.GetInfoRsp, error) {
rsp := &store.GetInfoRsp{
GiftInfoList: make([]*store.GiftInfo, 0),
LimitInfoList: make([]*store.DailyLimitInfo, 0),
}
// 业务逻辑
return rsp, nil
}
11. 测试与调试 (Testing & Debugging)
11.2 空跑模式
// ✅ 定时任务支持预演(apply = false)
func SendActEndMsg(apply bool) int {
// ...
if !apply {
count++
continue
}
// 实际发送消息
}
12. 业务特定模式 (Business Patterns)
12.1 自选奖励模式
// ✅ 任务完成 → 用户选择 → 扣减限量 → 发放奖励
// 1. 检查任务状态
taskInfo, err := taskSrv.GetTaskInfo(actInfo, task, uid, t)
if taskInfo.State != taskDef.TaskStatusCompleted {
return rsp, acterror.NewCustomError(actmsg.FrequentOperation)
}
// 2. 分布式锁保护限量
lock := actutil.TryLockWithRetryWithSuffix(conf.ActID, stageID, index, 10)
defer lock.Unlock()
// 3. 检查库存
currNum, _ := store.DailyLimit.BuildKey(ctx, stageID, date).HGet(index).Int()
if currNum >= dailyLimit {
return rsp, acterror.NewCustomError(actmsg.FrequentOperation)
}
// 4. 发放奖励(通过 weactivity.Reward → ParseRewards 转换后发放)
rewardInfos := actreward.ParseRewards(uid, configRewards)
actreward.SendRewards(uid, rewardInfos, &actreward.ExtraInfo{ActId: conf.ActID})
12.2 概率计算模式
涉及权重/概率计算时,必须优先使用 helper.GetRandomNumber,禁止自行实现随机权重逻辑。
// ✅ 正确:使用 helper.GetRandomNumber 进行权重随机
weightMap := map[int]int{
1: 50, // id=1,权重50
2: 30, // id=2,权重30
3: 20, // id=3,权重20
}
resultId := helper.GetRandomNumber(weightMap)
// ❌ 错误:自行实现随机权重逻辑
totalWeight := 0
for _, w := range weights {
totalWeight += w
}
randNum := rand.Intn(totalWeight)
// ... 手动遍历累加判断
12.3 奖励发放规范
发放奖励时,禁止直接手动拼接 actreward.RewardInfo,应通过配置中的 weactivity.Reward 或自行构造 weactivity.Reward,再通过 actreward.ParseRewards 转换后发放。
// ✅ 正确:通过配置中的 weactivity.Reward 转换发放
// 方式1:直接使用配置中的 []*weactivity.Reward
rewardInfos := actreward.ParseRewards(uid, specialConfig.Rewards)
err := actreward.SendRewards(uid, rewardInfos, &actreward.ExtraInfo{ActId: conf.ActID})
// 方式2:手动构造 weactivity.Reward 后转换
rewards := []*weactivity.Reward{
{
RewardType: "prop",
RewardId: 10001,
FemaleRewardId: 10002, // 选填,如果男女奖励不一样
RewardVal: 1,
ActId: conf.ActID,
},
}
rewardInfos := actreward.ParseRewards(uid, rewards)
err := actreward.SendRewards(uid, rewardInfos, &actreward.ExtraInfo{ActId: conf.ActID})
// ❌ 错误:直接拼接 actreward.RewardInfo 发放
rewardInfos := []*actreward.RewardInfo{
{
RewardType: "prop",
RewardId: 10001,
RewardVal: 1,
},
}
actreward.SendRewards(uid, rewardInfos, &actreward.ExtraInfo{ActId: conf.ActID})
12.4 全服弹幕规范
全服弹幕分为两种使用场景:
场景一:获得奖励时发送全服弹幕
当用户通过抽奖等方式获得高等级奖励时,使用 actnotify.SendRewardFullNotify,根据 RewardLevel 自动筛选(默认 RewardLevel == 3 才发送)。
// ✅ 获得奖励后发送全服弹幕(抽奖组件内已内置,手写时参考此模式)
func SendLotteryFullNotify(actInfo *weactivity.Activity, uid int32, gifts []*actreward.RewardInfo) {
if actInfo == nil {
return
}
notifyRewards := gifts
if helper.InActWhiteUids(uid) {
notifyRewards = actreward.MergeRewards(int(uid), gifts)
}
text1 := actmsg.GetMsgByKey(actmsg.FullNotifyText1)
text2 := actmsg.GetMsgByKey(actmsg.FullNotifyText2, actInfo.Name)
text3 := actmsg.GetMsgByKey(actmsg.FullNotifyText3)
bgUrl := actnotify.GetBgUrl()
btnUrl := actnotify.GetBtnUrl()
deepLink := helper.GetAppRedirectUrl(actInfo.ActUrl, "")
for _, gift := range notifyRewards {
if gift.RewardLevel != 3 {
continue
}
actnotify.SendFullNotifyV2(gift.RewardId, int(uid), gift.Broadcast, text1, text2, text3, deepLink, bgUrl, btnUrl)
}
}
场景二:自定义文案的全服弹幕
当需要发送自定义内容(非奖励触发)的全服弹幕时,通过配置 weactivity.FullNotify 结构体,使用 actnotify.TransformToFullNotify 转换后调用 .Do(uid, params) 发送。
// ✅ 配置结构体中定义 FullNotify(在 special_info 中配置)
type HuntConfig struct {
CoinNotify *weactivity.FullNotify `json:"coin_notify"`
}
// ✅ 使用 TransformToFullNotify 转换 + Do 发送
notify := actnotify.TransformToFullNotify(config.HuntConfig.CoinNotify)
notify.Do(uid, map[string]string{
"Uid": strconv.Itoa(uid),
"Coin": strconv.Itoa(coinAmount),
})
12.5 结构体深拷贝
一般结构体的深拷贝必须使用 helper.GobDeepCopy,禁止手动逐字段复制或使用 json.Marshal/Unmarshal 做深拷贝。
// ✅ 正确:使用 helper.GobDeepCopy
dst := new(TargetStruct)
_ = helper.GobDeepCopy(dst, src)
// ✅ 实际使用示例(FullNotify 深拷贝)
func TransformToFullNotify(notify *weactivity.FullNotify) *FullNotify {
dst := new(FullNotify)
_ = helper.GobDeepCopy(dst, notify)
return dst
}
// ❌ 错误:使用 json 做深拷贝(性能差且类型信息丢失)
data, _ := json.Marshal(src)
dst := new(TargetStruct)
json.Unmarshal(data, dst)
13. 注释规范 (Comments)
// ✅ 包注释:描述模块职责
// Package event 处理活动相关的异步事件,包括登录、送礼、成长值变更等
// ✅ 常量分组注释
const (
// 活动基础配置
ActID = 5811
GrowthChipID = 7
// 排行榜类型
RankTypeGrowth = "growth"
RankTypeRoom = "room"
)
// ✅ 复杂逻辑内联注释
// 累计次数:按本次礼物数量累加
if _, err := store.UserSendGiftInfo.BuildKey(ctx, uid, giftId).HIncrBy(store.FieldTotal(), number).Result(); err != nil {
// ...
}
// 最高连击:若本次连击大于历史最高则覆盖
if newCombo > oldCombo {
// ...
}
14. 反模式 (Anti-Patterns)
❌ 避免硬编码
// ❌ 错误
store.UserSendGiftInfo.BuildKey(ctx, uid, giftId).HGet("combo_times")
// ✅ 正确
store.UserSendGiftInfo.BuildKey(ctx, uid, giftId).HGet(store.FieldCombo())
❌ 避免深层嵌套
// ❌ 错误(4 层嵌套)
if actInfo != nil {
if specialInfo != nil {
if taskInfo != nil {
if taskInfo.State == completed {
// 业务逻辑
}
}
}
}
// ✅ 正确(早返回)
if actInfo == nil {
return
}
if specialInfo == nil {
return
}
if taskInfo == nil || taskInfo.State != completed {
return
}
// 业务逻辑
❌ 避免裸返回值
// ❌ 错误
func GetUserLevel(uid int) int {
// ...
return 0 // 无法区分是真实等级 0 还是错误
}
// ✅ 正确
func GetUserLevel(uid int) (int, error) {
// ...
return level, nil
}
❌ 避免魔法数字
// ❌ 错误
func GetRankLimit() int {
// ...
return 500
}
// ✅ 正确
func GetRankLimit() int {
// ...
return conf.DefaultRankLimit
}
15. Checklist(代码自查清单)
开发完成后,对照以下清单自查:
- 命名规范:常量、函数、变量命名符合规范
- 错误处理:所有 error 均有处理或日志
- 并发安全:共享资源使用分布式锁或原子操作
- 日志完整:关键节点有 Info 日志,错误有 Error 日志
- 配置校验:业务逻辑前校验配置/活动有效性
- 配置解析:GetSpecialConfig 内部使用
actinfo.ParseSpecialInfoV2 - Redis Key:使用 actredis 统一管理,无硬编码
- 资源释放:defer 释放锁/连接
- 概率计算:使用
helper.GetRandomNumber,禁止自行实现权重随机 - 奖励发放:通过
weactivity.Reward+actreward.ParseRewards转换,禁止直接拼接RewardInfo - 全服弹幕:奖励弹幕用
SendRewardFullNotify/SendFullNotifyV2,自定义文案用TransformToFullNotify+.Do() - 深拷贝:结构体深拷贝使用
helper.GobDeepCopy - SpecialConfig 设计:奖励ID、概率权重、倍率、时长等可配置数值放入 SpecialConfig(用
[]*weactivity.Reward等),禁止硬编码为 Go 常量 - 分布式锁:使用
actutil.TryLockUidWithRetry等封装函数,禁止直接使用actstore.SetExNx - Hook 注册:通过
GetHookers()返回actmodel.ActHookerList注册,禁止在init()中调用全局RegisterXxxHooker
16. 错题本(Mistake Notebook)
本 skill 附带一个持续更新的错题本,收录开发中真实发生的规范违规案例。
目录位置: mistake-notebook/entries/
| 文件 | 内容 |
|---|---|
naming.md |
命名规范错误(Field 硬编码、Resp/Rsp 混用、常量未分组等) |
error-handling.md |
错误处理错误(忽略 error、缺少活动校验、日志无上下文等) |
concurrency.md |
并发与锁错误(缺锁、未检查 lock.Failed、裸 goroutine 等) |
redis.md |
Redis 使用错误(Key 硬编码、读写竞态、参数数量不匹配等) |
structure.md |
结构设计错误(魔法数字、深层嵌套、空函数体、过度封装等) |
AI 执行指令
加载本 skill 时,同时读取 mistake-notebook/entries/ 下所有文件,在代码生成或 CR 时主动检查是否命中已知错误模式。
发现新的规范违规案例时,按 mistake-notebook/README.md 中的模板追加到对应分类文件,形成持续积累的错题库。